<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>DiyMediaServer</title><link>https://diymediaserver.com/</link><description>Recent content on DiyMediaServer</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Wed, 05 Aug 2026 14:14:27 -0600</lastBuildDate><atom:link href="https://diymediaserver.com/index.xml" rel="self" type="application/rss+xml"/><item><title>ntfy + Uptime Kuma on a VPS: Alerts and a Status Page</title><link>https://diymediaserver.com/post/2026/uptime-kuma-ntfy-vps-status-page/</link><pubDate>Thu, 30 Jul 2026 11:54:25 -0600</pubDate><guid>https://diymediaserver.com/post/2026/uptime-kuma-ntfy-vps-status-page/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/uptime-kuma-ntfy-vps-status-page/featured_hu_59831119d9553019.webp" alt="Featured image of post ntfy + Uptime Kuma on a VPS: Alerts and a Status Page" /&gt;&lt;p&gt;Every outage of my Jellyfin server starts a flood of text messages: &amp;ldquo;hey is the movie thing broken again?&amp;rdquo; I&amp;rsquo;m the status page. At 11pm I&amp;rsquo;m also asleep. The ntfy and Uptime Kuma pair below fixed this, but only after I moved both of them off my own hardware.&lt;/p&gt;
&lt;p&gt;Before moving ntfy and Kuma to a VPS, I ran Uptime Kuma and my ntfy server on a local Docker VM, the same Docker VM hosting half the services they were watching. When that VM died or my internet went out, the monitor and the alert to my phone died with it. The lab failed silently. Except for the text messages asking what happened.&lt;/p&gt;
&lt;p&gt;The fix was one cheap VPS running two containers. ntfy pushes to my phone. Uptime Kuma watches the real public path from outside the house and publishes a self-hosted status page my family and friends can check without texting me. So here&amp;rsquo;s the whole build. The Uptime Kuma Compose file. The Caddy config that keeps the admin dashboard off the internet while the status page stays public. ntfy&amp;rsquo;s deny-all auth model. The backups almost nobody sets up. And the tests that prove any of it actually works.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Running Uptime Kuma and ntfy on a cheap VPS behind Caddy, with both containers bound to loopback, puts your alerting outside the failure domain it monitors. You get push notifications that survive problems in your lab; your users get a public status page URL instead of texting you.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Docker&lt;/dt&gt;&lt;dd&gt;29.6.2&lt;/dd&gt;&lt;dt&gt;Caddy&lt;/dt&gt;&lt;dd&gt;2.11.4&lt;/dd&gt;&lt;dt&gt;Ntfy&lt;/dt&gt;&lt;dd&gt;2.26.0 (binwiederhier/ntfy:v2)&lt;/dd&gt;&lt;dt&gt;Uptime kuma&lt;/dt&gt;&lt;dd&gt;2.4.0 (louislam/uptime-kuma:2)&lt;/dd&gt;&lt;dt&gt;Vps&lt;/dt&gt;&lt;dd&gt;5 vCPU / 5.9 GB RAM, 89 GB disk&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-07-30&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I moved both services onto a RackNerd VPS with 5 vCPU, 5.9 GB of RAM (way more than you need for this) and Docker 29.6.2 on 2026-07-16, running &lt;code&gt;louislam/uptime-kuma:2&lt;/code&gt; (2.4.0 at the time) and &lt;code&gt;binwiederhier/ntfy:v2&lt;/code&gt; (2.26.0). Every config block below is the sanitized version of what&amp;rsquo;s running there right now, along with the two failures that cost me the most time: a silent 403 that hid a dead alert path for a few days, and a Caddy reload that hung for about a half hour.&lt;/p&gt;
&lt;h2 id="why-ntfy-and-uptime-kuma-belong-outside-your-lab"&gt;Why ntfy and Uptime Kuma Belong Outside Your Lab
&lt;/h2&gt;&lt;p&gt;A monitor that shares a failure domain with the thing it monitors can&amp;rsquo;t accurately report on it. That&amp;rsquo;s the whole point here, and it&amp;rsquo;s why &amp;ldquo;I already run Uptime Kuma&amp;rdquo; isn&amp;rsquo;t an answer.&lt;/p&gt;
&lt;p&gt;If Kuma and your notification server both run on the same hypervisor, the same VM, the same power strip, or behind the same ISP connection, they&amp;rsquo;re one failure away from useless at the exact moment you need them. Power cut, failed PVE node, a WireGuard tunnel that never comes back after a reboot: in all three cases the monitor is dead too, and the silence looks identical to everything being fine.&lt;/p&gt;
&lt;p&gt;Put both containers on a VPS in someone else&amp;rsquo;s data center and they share fate only with your public front door. Lose power at your house, VPS keeps probing, phone still buzzes.&lt;/p&gt;
&lt;p&gt;The second payoff is where you&amp;rsquo;re watching from. A check against &lt;code&gt;https://flix.example.com/&lt;/code&gt; from outside exercises public DNS, the VPS reverse proxy, the tunnel back to the lab, and the backend service in one request. A LAN-side check against &lt;code&gt;http://192.168.1.4:8096&lt;/code&gt; exercises none of those four. It stays green while every one of them is broken.&lt;/p&gt;
&lt;h3 id="two-audiences-two-very-different-outputs"&gt;Two Audiences, Two Very Different Outputs
&lt;/h3&gt;&lt;p&gt;There are two definitions of &amp;ldquo;visibility&amp;rdquo; in play here, and it helps to keep them apart:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You&lt;/strong&gt; get push notifications on your phone. Immediate, detailed, noisy on purpose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your users&lt;/strong&gt; (the four people on your Jellyfin) get a URL. Passive, deliberately vague, no app and no login. They can check for themselves whether everything&amp;rsquo;s working, even at 3am.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cost is one small VPS, a domain you probably already own, and two containers that together use well under a gigabyte of RAM.&lt;/p&gt;
&lt;p&gt;Now the trade-off: this design gives up LAN-vantage monitoring. When I retired the internal instance I deliberately didn&amp;rsquo;t recreate its per-host up/down checks. Three outside-in monitors are my complete set. Per-host LAN monitoring is a blind spot I chose to live with. I&amp;rsquo;d rather say that out loud than pretend otherwise.&lt;/p&gt;
&lt;h2 id="the-architecture-before-you-type-anything"&gt;The Architecture, Before You Type Anything
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what this looks like. Phones and browsers hit Caddy on the VPS at port 443. Caddy proxies to two loopback-bound containers: &lt;code&gt;127.0.0.1:3001&lt;/code&gt; for Uptime Kuma and &lt;code&gt;127.0.0.1:3080&lt;/code&gt; for ntfy. Kuma&amp;rsquo;s monitors go to your public hostnames, the same way any browser would. Kuma&amp;rsquo;s notifications go to ntfy over the Docker network and never touch the internet at all.&lt;/p&gt;
&lt;p&gt;Two public names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status.example.com&lt;/code&gt;, the status page, served through a strict path allowlist.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ntfy.example.com&lt;/code&gt;, the full ntfy API, protected by auth rather than by path rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kuma&amp;rsquo;s admin dashboard is never published anywhere. It lives on the same port as the status page, and the only way in is via an SSH tunnel.&lt;/p&gt;
&lt;p&gt;Prerequisites: a VPS with Docker installed, a domain whose DNS you control, Caddy on the VPS, and ports 80 and 443 open. You also need a WireGuard tunnel back to the lab if you want to monitor anything that isn&amp;rsquo;t publicly exposed. Be clear with yourself about that last one. The first two monitors in this build work fine without a tunnel. The third one doesn&amp;rsquo;t. No tunnel means you&amp;rsquo;re building the two-monitor version of this guide. If you haven&amp;rsquo;t built a WireGuard tunnel before, my &lt;a class="link" href="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/" &gt;WireGuard VPS to homelab guide&lt;/a&gt; covers it, and the &lt;a class="link" href="https://diymediaserver.com/post/2026/nginx-proxy-manager-to-caddy-migration/" &gt;Nginx Proxy Manager to Caddy migration post&lt;/a&gt; covers the reverse proxy side.&lt;/p&gt;
&lt;h2 id="the-uptime-kuma-docker-compose-file-that-keeps-admin-off-the-internet"&gt;The Uptime Kuma Docker Compose File That Keeps Admin Off the Internet
&lt;/h2&gt;&lt;p&gt;Both services live in one compose project. I keep mine at &lt;code&gt;/docker/compose.yml&lt;/code&gt; with app data under &lt;code&gt;/docker/&amp;lt;app&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uptime-kuma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;louislam/uptime-kuma:2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;uptime-kuma&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:3001:3001&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./uptime-kuma/data:/app/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ntfy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;binwiederhier/ntfy:v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ntfy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:3080:80&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;NTFY_BASE_URL=https://ntfy.example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;NTFY_BEHIND_PROXY=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;NTFY_KEEPALIVE_INTERVAL=45s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;NTFY_AUTH_FILE=/var/lib/ntfy/user.db&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;NTFY_AUTH_DEFAULT_ACCESS=deny-all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/docker/ntfy/var/cache/ntfy:/var/cache/ntfy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/docker/ntfy/etc/ntfy:/etc/ntfy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/docker/ntfy/var/lib/ntfy:/var/lib/ntfy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;CMD-SHELL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo &amp;#39;\&amp;#34;healthy\&amp;#34;\\s*:\\s*true&amp;#39; || exit 1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;60s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;10s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;40s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the &lt;code&gt;.env&lt;/code&gt; file that &lt;code&gt;${TZ}&lt;/code&gt; reads from before the first start:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;TZ=America/Denver&amp;#39;&lt;/span&gt; &amp;gt; /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Put your own time zone in that file. Skip it and Compose starts the container with an empty &lt;code&gt;TZ&lt;/code&gt;. Every ntfy timestamp then lands in UTC, and you get to do timezone math on every alert you read.&lt;/p&gt;
&lt;p&gt;Bring it up with &lt;code&gt;docker compose up -d&lt;/code&gt;, then confirm the binds with &lt;code&gt;docker port uptime-kuma&lt;/code&gt; and &lt;code&gt;ss -tlnp&lt;/code&gt;. Both should show loopback only.&lt;/p&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
The loopback bind is the security boundary, not your firewall. Docker publishes ports by writing its own forwarding rules that bypass the host&amp;rsquo;s firewall. A default-drop iptables or ufw policy won&amp;rsquo;t save you. Publish Kuma as &lt;code&gt;3001:3001&lt;/code&gt; and you&amp;rsquo;ve put a full admin UI with a login form on the public internet, firewall or not. &lt;code&gt;127.0.0.1:3001:3001&lt;/code&gt; is what keeps it private.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Three things in that file deserve an explanation.&lt;/p&gt;
&lt;p&gt;The image tag is &lt;code&gt;:2&lt;/code&gt;, not &lt;code&gt;:latest&lt;/code&gt;. Docker Hub&amp;rsquo;s &lt;code&gt;latest&lt;/code&gt; tag for Uptime Kuma still tracks the old 1.x line. The floating major tag follows every 2.x release, which is what you want from routine &lt;code&gt;docker compose pull&lt;/code&gt; updates. A future bump to &lt;code&gt;:3&lt;/code&gt; should be a deliberate edit, because a new major version means re-auditing the Caddy allowlist later in this post against the new release&amp;rsquo;s status page router.&lt;/p&gt;
&lt;p&gt;ntfy runs on &lt;code&gt;:v2&lt;/code&gt; for the same reason. Same floating-major logic, same deliberate upgrade path.&lt;/p&gt;
&lt;p&gt;Persist &lt;code&gt;/var/lib/ntfy&lt;/code&gt;. That volume holds &lt;code&gt;user.db&lt;/code&gt;, which is every ntfy user, token, and ACL you&amp;rsquo;ll create in the next section. Without it, every container recreation wipes your auth and every producer you own needs reconfiguring. With it, moving ntfy between hosts is a directory copy. My migration off the lab VM onto the VPS took zero client reconfiguration, because &lt;code&gt;user.db&lt;/code&gt; came along for the ride and my phone and scripts never noticed the server had moved.&lt;/p&gt;
&lt;h2 id="dns-first-then-caddy-in-that-order"&gt;DNS First, Then Caddy, In That Order
&lt;/h2&gt;&lt;p&gt;Create the A records for &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;ntfy&lt;/code&gt; pointing at the VPS &lt;strong&gt;before&lt;/strong&gt; your first Caddy deploy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig +short status.example.com @1.1.1.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dig +short ntfy.example.com @1.1.1.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both must return the VPS address. Caddy issues certificates over the HTTP-01 challenge and can&amp;rsquo;t get a certificate for a name that doesn&amp;rsquo;t resolve. Getting this order backwards is the most common first-run failure, and the error message points at TLS rather than at DNS.&lt;/p&gt;
&lt;p&gt;The ntfy vhost is an ordinary reverse proxy with one directive that isn&amp;rsquo;t obvious:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;ntfy.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SAMEORIGIN&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nosniff&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;-Server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3080&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;flush_interval&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;flush_interval -1&lt;/code&gt; disables response buffering. A buffering proxy sits on your messages until the buffer fills, then delivers them in a clump, minutes after the event they were warning you about.&lt;/p&gt;
&lt;p&gt;That block and the status page block further down both live in &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;. Apply them with &lt;code&gt;sudo systemctl reload caddy&lt;/code&gt;. If you want to check the syntax first, run the check as the &lt;code&gt;caddy&lt;/code&gt; user rather than as root: &lt;code&gt;sudo -u caddy caddy validate --config /etc/caddy/Caddyfile&lt;/code&gt;. The reason why is a 30-minute story in the troubleshooting section.&lt;/p&gt;
&lt;h2 id="locking-down-ntfy-deny-all-plus-write-only-tokens"&gt;Locking Down ntfy: deny-all Plus Write-Only Tokens
&lt;/h2&gt;&lt;p&gt;Out of the box, ntfy is anonymous read-write. On an internet-facing instance that means anyone who guesses or scrapes a topic name can read your alerts. Mine include WAN IP change notifications, so an open instance would leak my WAN IP history to whoever found the topic.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NTFY_AUTH_DEFAULT_ACCESS=deny-all&lt;/code&gt; in the compose environment above shuts that off. But now nothing can publish either, so you need two accounts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy user add --role&lt;span class="o"&gt;=&lt;/span&gt;admin owner
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy user add publisher
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;owner&lt;/code&gt; is an admin account that bypasses ACLs. This is what the phone app and the web app log in as.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publisher&lt;/code&gt; is a plain user granted write-only access on each topic. Every automation uses this account.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Grant the per-topic access, then mint one labeled token per producer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy access publisher KumaUptime write-only
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy access publisher Backups write-only
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy token add --label kuma-vps publisher
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One token per producer, always. When a script gets compromised you revoke that one token and nothing else in your setup notices. Write-only is the other half: a stolen producer token can publish noise, but it can&amp;rsquo;t read the alert history it was never meant to see.&lt;/p&gt;
&lt;p&gt;My topic layout, one topic per concern so the phone can mute categories independently:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;What publishes to it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KumaUptime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uptime Kuma monitor state changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Backups&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backup job failures and stale-archive warnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Storage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SMART health and pool free space from the storage box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WAN IP changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fail2ban bans on the VPS and on Jellyfin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;Lessons learned:&lt;/strong&gt; the &lt;code&gt;Authorization&lt;/code&gt; header must read &lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;, with the prefix. Two of my webhook producers carried the bare token and silently returned 403 for days. Because those alerts are failure-only, a completely dead notification path looked exactly like a healthy quiet one. A silent alert channel is worse than no alert channel, because you trust it. After creating any token, publish a real test message and confirm it arrives.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;One more expectation to set. ntfy&amp;rsquo;s message cache lives in memory unless you set &lt;code&gt;NTFY_CACHE_FILE&lt;/code&gt;. Connected subscribers get everything live, but history disappears the moment the container restarts. I decided I didn&amp;rsquo;t need durable history and moved on. If you do want it, set the cache file now rather than finding out the hard way after a restart.&lt;/p&gt;
&lt;h2 id="first-run-of-uptime-kuma-over-an-ssh-tunnel"&gt;First Run of Uptime Kuma Over an SSH Tunnel
&lt;/h2&gt;&lt;p&gt;The admin UI sits on &lt;code&gt;127.0.0.1:3001&lt;/code&gt; and never gets published. To reach it, forward a local port to the VPS loopback:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh -N -L 13001:127.0.0.1:3001 &amp;lt;user&amp;gt;@203.0.113.10
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Leave that running and open &lt;code&gt;http://127.0.0.1:13001&lt;/code&gt; in your browser to complete the first-run admin account setup.&lt;/p&gt;
&lt;p&gt;The flags: &lt;code&gt;-N&lt;/code&gt; opens the tunnel without a remote shell, and &lt;code&gt;-L 13001:127.0.0.1:3001&lt;/code&gt; maps local port 13001 to the VPS&amp;rsquo;s own loopback port 3001. Pick any free local port. Kuma only ever sees a connection originating from &lt;code&gt;127.0.0.1&lt;/code&gt; on the VPS. Nothing is exposed to the internet. Close the tunnel with Ctrl-C when you&amp;rsquo;re done.&lt;/p&gt;
&lt;p&gt;Put the Kuma admin credentials in your password manager. That account can rewrite every monitor and read every notification target you configure, including the ntfy token.&lt;/p&gt;
&lt;p&gt;While you&amp;rsquo;re in there, open &lt;strong&gt;Settings → Reverse Proxy&lt;/strong&gt; and enable &lt;strong&gt;Trust Proxy&lt;/strong&gt; so Kuma reads &lt;code&gt;X-Forwarded-For&lt;/code&gt; and shows real client addresses rather than the proxy&amp;rsquo;s.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0GR1BY1SZ"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/macbook_hu_2a1a011958cd5e86.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/macbook_hu_a386a7d25c9bf5a4.webp" width="600" height="364" alt="Apple 2026 MacBook Air 13-inch Laptop with M5 chip" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Apple 2026 MacBook Air 13-inch Laptop with M5 chip.&lt;/strong&gt;
This ultra-portable powerhouse features the latest Apple silicon paired with 24GB of unified memory and a massive 1TB SSD, delivering desktop-class performance for heavy multitasking and professional creative workflows. Its fanless, razor-thin design and brilliant 13.6-inch Liquid Retina display make it the ultimate mobile workstation for those who refuse to compromise on speed or storage.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/48MaPin" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="monitors-that-measure-what-your-users-experience"&gt;Monitors That Measure What Your Users Experience
&lt;/h2&gt;&lt;p&gt;Monitor the public hostname, never the LAN IP. Only the public path exercises DNS, the reverse proxy, the tunnel, and the backend together, and that makes it the only check that can catch an outage your users would actually see.&lt;/p&gt;
&lt;p&gt;My three monitors, and why each one exists:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Jellyfin Server&lt;/strong&gt; (&lt;code&gt;https://flix.example.com/&lt;/code&gt;), the thing people actually complain about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jellyseerr&lt;/strong&gt; (&lt;code&gt;https://request.example.com/&lt;/code&gt;), the request front end, because &amp;ldquo;I can&amp;rsquo;t add movies&amp;rdquo; is the second most common message I get.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Xfinity&lt;/strong&gt;, a ping monitor aimed at a LAN-side target over the WireGuard tunnel. The name is my ISP&amp;rsquo;s, and that&amp;rsquo;s all it is: a WAN reachability check answering &amp;ldquo;is the internet at the house working&amp;rdquo; without publishing my home IP on a public page. This is the monitor that needs the tunnel. No tunnel, no third monitor.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Three monitors, not thirty. Every monitor on a public status page is a line item that &lt;strong&gt;anyone&lt;/strong&gt; can read. Restraint here is a security decision as much as a design one.&lt;/p&gt;
&lt;p&gt;For interval and retries, pick numbers that catch a real outage without paging you for a two-second blip. A 60 second interval with retries set to 2 or 3 before the notification fires is a reasonable starting point. Tune it after the first week of real data.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C4GWPYMB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/beelink_hu_5974274794f3200a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/beelink_hu_dd7f73b5fd08d717.webp" width="600" height="481" alt="Beelink SER5 (Ryzen 5 5500U)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Beelink SER5 (Ryzen 5 5500U).&lt;/strong&gt;
A palm-sized mini PC with a 6-core/12-thread CPU that feels snappy for everyday work and homelab duties, handling Docker stacks, light VMs, and Jellyfin without guzzling power. With NVMe plus a 2.5″ bay, Wi-Fi 6, and multi-display output, it&amp;rsquo;s a quiet, tidy upgrade for desk or media setups.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4fB1X31" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="building-a-self-hosted-status-page-worth-checking"&gt;Building a Self-Hosted Status Page Worth Checking
&lt;/h2&gt;&lt;p&gt;In Kuma, go to &lt;strong&gt;Status Pages&lt;/strong&gt; and create one published page. Mine uses the slug &lt;code&gt;media&lt;/code&gt;, the title &amp;ldquo;Jellyfin Media Status&amp;rdquo;, and one group called Services holding the two monitors non-technical people care about.&lt;/p&gt;
&lt;p&gt;Write the display names for humans. &amp;ldquo;Jellyfin Server&amp;rdquo;, not &amp;ldquo;jf-lxc-04 http 8096&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;What to leave off the page: hostnames, IP addresses, port numbers, internal service names, and anything else that reads like an inventory.&lt;/p&gt;
&lt;p&gt;The payoff is one link sent to the family. Green means everything is up and running, red means it&amp;rsquo;s broken and I already know (don&amp;rsquo;t text me about it). The volume of text messages dropped immediately. A private dashboard only you can see would have done nothing for that.&lt;/p&gt;
&lt;p&gt;Write down the slug. You need it for the Caddy config in the next section and again for restores. Rebuild Kuma from scratch with a different slug and the redirect and the allowlist both break.&lt;/p&gt;
&lt;h2 id="the-caddy-allowlist-that-keeps-the-admin-ui-off-the-internet"&gt;The Caddy Allowlist That Keeps the Admin UI Off the Internet
&lt;/h2&gt;&lt;p&gt;This is the most important config in the post.&lt;/p&gt;
&lt;p&gt;Uptime Kuma serves its admin dashboard and its public status page from the same port. Proxy the whole vhost and you&amp;rsquo;ve published the login form, the setup wizard, the Socket.IO endpoint, the upload directory, and the per-monitor badge API. The answer is an allowlist, not a blocklist: name the exact routes the status page needs, proxy only GET and HEAD on those, and 404 everything else at the proxy so it never reaches Kuma at all.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;status.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;log&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt; &lt;span class="s"&gt;/var/log/caddy/status.access.log&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;roll_size&lt;/span&gt; &lt;span class="s"&gt;100mb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;roll_keep&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;format&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;redir&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="nd"&gt;/status/media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@media_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;method&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;path&lt;/span&gt; &lt;span class="s"&gt;/status/media&lt;/span&gt; &lt;span class="s"&gt;/status/media/&lt;/span&gt; &lt;span class="s"&gt;/status/media/rss&lt;/span&gt; &lt;span class="s"&gt;/api/status-page/media&lt;/span&gt; &lt;span class="s"&gt;/api/status-page/heartbeat/media&lt;/span&gt; &lt;span class="s"&gt;/api/status-page/media/manifest.json&lt;/span&gt; &lt;span class="s"&gt;/api/status-page/media/incident-history&lt;/span&gt; &lt;span class="s"&gt;/assets/*&lt;/span&gt; &lt;span class="s"&gt;/icon.svg&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;handle&lt;/span&gt; &lt;span class="nd"&gt;@media_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;respond&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Walking the pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redir / /status/media&lt;/code&gt; exists because the bare root would otherwise fall through to the 404 handler.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;method GET HEAD&lt;/code&gt; because a status page never needs to POST anything. Every write method dies at the proxy.&lt;/li&gt;
&lt;li&gt;The explicit &lt;code&gt;/api/status-page/*&lt;/code&gt; paths are how the page fetches its config, its heartbeats, its manifest, and its incident history. Miss one and the page renders but never updates.&lt;/li&gt;
&lt;li&gt;The trailing &lt;code&gt;handle { respond 404 }&lt;/code&gt; catches everything else: &lt;code&gt;/dashboard&lt;/code&gt;, &lt;code&gt;/socket.io/&lt;/code&gt;, &lt;code&gt;/api/badge/*&lt;/code&gt;, &lt;code&gt;/upload/*&lt;/code&gt;, any other status page slug, and every non-GET request.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Badges get denied for a specific reason: &lt;code&gt;/api/badge/&amp;lt;id&amp;gt;/status&lt;/code&gt; leaks per-monitor state by numeric ID, including monitors that aren&amp;rsquo;t on the public page.&lt;/p&gt;
&lt;p&gt;This allowlist fails closed. If a future Kuma release adds a route the status page needs, a page feature breaks and you notice immediately. A blocklist fails the other way: a new admin route appears and stays public until you happen to look at it. That asymmetry is why I reach for allowlists.&lt;/p&gt;
&lt;p&gt;I audited this route list against the Kuma 2.4.0 status page router before deploying it. Re-audit on any major version bump, and write that down somewhere instead of trusting your memory.&lt;/p&gt;
&lt;p&gt;One thing makes a path allowlist viable at all: the status page doesn&amp;rsquo;t need a websocket. Heartbeats arrive over REST. Verify it on your own instance before you trust me. Load the status page with browser devtools open and confirm the heartbeat bars populate with no websocket connection.&lt;/p&gt;
&lt;h2 id="connect-uptime-kuma-to-ntfy-the-right-way"&gt;Connect Uptime Kuma to ntfy the Right Way
&lt;/h2&gt;&lt;p&gt;In Kuma, add a notification of type &lt;strong&gt;ntfy&lt;/strong&gt;. The setting to get right is the server URL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server URL: &lt;code&gt;http://ntfy&lt;/code&gt; (the container name on the shared compose network)&lt;/li&gt;
&lt;li&gt;Topic: &lt;code&gt;KumaUptime&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Priority: high&lt;/li&gt;
&lt;li&gt;Access token: the &lt;code&gt;kuma-vps&lt;/code&gt; token you minted earlier&lt;/li&gt;
&lt;li&gt;Set as default, apply to all existing monitors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use the container name. The public URL is the tempting choice and the wrong one. Container-to-container means alerting still works when Caddy is down, when DNS is broken, when the certificate has expired, and when the entire lab has gone dark. There&amp;rsquo;s almost nothing left in the alert path that can fail on its own.&lt;/p&gt;
&lt;p&gt;Hit &lt;strong&gt;Test&lt;/strong&gt; and confirm the push lands on your phone before you move on. Don&amp;rsquo;t skip this. That exact assumption is how I ended up with two producers that had been dead for days.&lt;/p&gt;
&lt;h2 id="prove-it-do-not-assume-it"&gt;Prove It, Do Not Assume It
&lt;/h2&gt;&lt;p&gt;Run every line of this from a machine that isn&amp;rsquo;t the VPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sSI https://status.example.com/ &lt;span class="p"&gt;|&lt;/span&gt; head -n &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# 302 to /status/media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; https://status.example.com/status/media &lt;span class="c1"&gt;# 200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; https://status.example.com/dashboard &lt;span class="c1"&gt;# 404&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; https://status.example.com/socket.io/ &lt;span class="c1"&gt;# 404&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; https://status.example.com/api/badge/1/status &lt;span class="c1"&gt;# 404&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nc -zv -w5 203.0.113.10 &lt;span class="m"&gt;3001&lt;/span&gt; &lt;span class="c1"&gt;# must NOT connect&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;test&amp;#34;&lt;/span&gt; https://ntfy.example.com/KumaUptime &lt;span class="c1"&gt;# 403&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;About that &lt;code&gt;nc&lt;/code&gt; line: both &amp;ldquo;Connection timed out&amp;rdquo; and &amp;ldquo;Connection refused&amp;rdquo; are passes. A timeout means a DROP-style firewall is swallowing the packet. A refusal means REJECT, or nothing listening on the public interface. Either way the port isn&amp;rsquo;t reachable. What you must never see is &lt;code&gt;succeeded&lt;/code&gt;. If it connects, stop and fix that before you do anything else.&lt;/p&gt;
&lt;p&gt;Then repeat the last curl with &lt;code&gt;-H &amp;quot;Authorization: Bearer &amp;lt;token&amp;gt;&amp;quot;&lt;/code&gt; and confirm it returns 200.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;strong&gt;The real acceptance test:&lt;/strong&gt; stop the Jellyfin container and confirm your phone buzzes. This is the exact failure the old topology could never report. If your phone stays quiet, something in the chain is broken. Head to the troubleshooting section below.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0DNF5JWB4"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-5_hu_221d2b10d350a1b8.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-5_hu_5b2e6d1d98e59035.webp" width="600" height="461" alt="Raspberry Pi 5 (8GB) kit" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Raspberry Pi 5 (8GB) kit.&lt;/strong&gt;
A Pi 5 8GB board bundled with a case, an active cooler, and a screwdriver, which covers the cooling and enclosure you&amp;rsquo;d otherwise source separately. The board is the biggest generational leap in Pi history. A quad-core Cortex-A76 at 2.4 GHz gives it roughly twice the CPU performance of a Pi 4, with PCIe 2.0 via the new FFC connector for NVMe storage, a real power button, and a dedicated Raspberry Pi RP1 I/O chip. It still sips power and fits in your hand, but now handles heavier Docker workloads, Pi-hole plus Home Assistant simultaneously, and even light Jellyfin duties.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4w9N5xn" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="back-up-both-sqlite-files-because-the-vps-is-not-in-your-backup-plan"&gt;Back Up Both SQLite Files, Because the VPS Is Not in Your Backup Plan
&lt;/h2&gt;&lt;p&gt;Two small databases now need to be part of your backup plan. &lt;code&gt;kuma.db&lt;/code&gt; holds your monitors, the admin account, the status page slug and its uploaded assets, and the ntfy token. &lt;code&gt;user.db&lt;/code&gt; holds every ntfy user, token, and ACL.&lt;/p&gt;
&lt;p&gt;Stage one runs on the VPS. This script takes a WAL-safe online copy without stopping the container, tars it up with the config and compose file, and keeps 14 dated archives at mode 0640, because both databases contain recoverable secrets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;data_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/docker/uptime-kuma/data&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;out_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/var/backups/kuma-vps&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;date_stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%F&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /root/.kuma-stage.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -rf &amp;#34;$stage_dir&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0750&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sqlite3 &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$data_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma.db&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.backup &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma/kuma.db&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -a &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$data_dir&lt;/span&gt;&lt;span class="s2"&gt;/db-config.json&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma/db-config.json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$data_dir&lt;/span&gt;&lt;span class="s2"&gt;/upload&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cp -a &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$data_dir&lt;/span&gt;&lt;span class="s2"&gt;/upload&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma/upload&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -a /docker/compose.yml &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma/compose.yml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;archive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;/kuma-&lt;/span&gt;&lt;span class="nv"&gt;$date_stamp&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -czf &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$archive&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; kuma
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0640&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$archive&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -1t &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;/kuma-*.tar.gz 2&amp;gt;/dev/null &lt;span class="p"&gt;|&lt;/span&gt; tail -n +15 &lt;span class="p"&gt;|&lt;/span&gt; xargs -r rm -f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The ntfy version is the same shape with different paths. Here it is in full, because the half of the pair that protects your auth tokens deserves better than &amp;ldquo;figure it out&amp;rdquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;data_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/docker/ntfy/var/lib/ntfy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;conf_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/docker/ntfy/etc/ntfy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;out_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/var/backups/ntfy-vps&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;date_stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%F&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /root/.ntfy-stage.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -rf &amp;#34;$stage_dir&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/ntfy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0750&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sqlite3 &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$data_dir&lt;/span&gt;&lt;span class="s2"&gt;/user.db&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.backup &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/ntfy/user.db&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$conf_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cp -a &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$conf_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/ntfy/etc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -a /docker/compose.yml &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;/ntfy/compose.yml&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;archive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;/ntfy-&lt;/span&gt;&lt;span class="nv"&gt;$date_stamp&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -czf &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$archive&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; ntfy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0640&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$archive&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -1t &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$out_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;/ntfy-*.tar.gz 2&amp;gt;/dev/null &lt;span class="p"&gt;|&lt;/span&gt; tail -n +15 &lt;span class="p"&gt;|&lt;/span&gt; xargs -r rm -f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both scripts call the host&amp;rsquo;s &lt;code&gt;sqlite3&lt;/code&gt; binary, so &lt;code&gt;apt install sqlite3&lt;/code&gt; on the VPS if it isn&amp;rsquo;t there. Schedule both in &lt;code&gt;/etc/cron.d&lt;/code&gt;, offset by ten minutes so they never collide:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;30 3 * * * root /usr/local/sbin/ntfy-backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;40 3 * * * root /usr/local/sbin/kuma-backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Stage two pulls those archives to a machine at home that &lt;em&gt;is&lt;/em&gt; inside your backup system:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;15 4 * * * youruser rsync -a -e &amp;#34;ssh -i /home/youruser/.ssh/vps_key&amp;#34; youruser@203.0.113.10:/var/backups/ntfy-vps/ /home/youruser/backups/ntfy-vps/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;25 4 * * * youruser rsync -a -e &amp;#34;ssh -i /home/youruser/.ssh/vps_key&amp;#34; youruser@203.0.113.10:/var/backups/kuma-vps/ /home/youruser/backups/kuma-vps/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No &lt;code&gt;--delete&lt;/code&gt; on that rsync. A VPS-side deletion should never propagate into the copy you&amp;rsquo;re keeping.&lt;/p&gt;
&lt;p&gt;Once a month, extract the newest pulled archive and check the database inside it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /home/youruser/backups/kuma-vps
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -xzf &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;ls -1t kuma-*.tar.gz &lt;span class="p"&gt;|&lt;/span&gt; head -n 1&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -C /tmp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sqlite3 /tmp/kuma/kuma.db &lt;span class="s1"&gt;&amp;#39;PRAGMA integrity_check;&amp;#39;&lt;/span&gt; &lt;span class="c1"&gt;# expect: ok&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anything other than &lt;code&gt;ok&lt;/code&gt; means that archive is scrap and you fall back to the previous one. If the pulling box doesn&amp;rsquo;t have the &lt;code&gt;sqlite3&lt;/code&gt; CLI, Python&amp;rsquo;s bundled module does the same job:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -c &lt;span class="s2"&gt;&amp;#34;import sqlite3;print(sqlite3.connect(&amp;#39;/tmp/kuma/kuma.db&amp;#39;).execute(&amp;#39;PRAGMA integrity_check&amp;#39;).fetchone()[0])&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run the same check against the newest &lt;code&gt;ntfy-*.tar.gz&lt;/code&gt; and &lt;code&gt;/tmp/ntfy/user.db&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The failure signal here is archive age. A backup pipeline that silently stops looks identical to a healthy one right up until the day you need it. I added a check that pages the &lt;code&gt;Backups&lt;/code&gt; topic when the newest pulled archive is missing or older than 26 hours.&lt;/p&gt;
&lt;p&gt;And yes, that&amp;rsquo;s circular. If ntfy is down, ntfy can&amp;rsquo;t tell you its own backup failed. A stale archive is still the ultimate signal, which is why the monthly manual check stays on the calendar.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07YD3G568"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/wd-14tb-external_hu_a375ac21ff6f20fc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/wd-14tb-external_hu_7547932251399905.webp" width="600" height="1293" alt="WD 14TB Elements Desktop External Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;WD 14TB Elements Desktop External Hard Drive.&lt;/strong&gt;
A desktop external drive that packs 14 TB into a USB 3.0 enclosure, making it one of the cheapest ways to add bulk storage to a homelab or grab a large drive for shucking into a NAS. WD&amp;rsquo;s CMR recording on these capacities means solid sustained write speeds, and the wide compatibility with Windows, Mac, and Linux makes it an instant-plug backup target.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4fKyPoH" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="restoring-either-database-on-a-rebuilt-vps"&gt;Restoring Either Database on a Rebuilt VPS
&lt;/h3&gt;&lt;p&gt;A backup you&amp;rsquo;ve never restored isn&amp;rsquo;t a backup, it&amp;rsquo;s a hope. Rehearse this once while nothing is broken. The middle of an outage is the worst possible time to learn the procedure.&lt;/p&gt;
&lt;p&gt;Copy the archive back to the VPS first, then restore Kuma:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose stop uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -xzf /root/kuma-2026-07-16.tar.gz -C /tmp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /tmp/kuma/kuma.db /docker/uptime-kuma/data/kuma.db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /tmp/kuma/db-config.json /docker/uptime-kuma/data/db-config.json
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -f /docker/uptime-kuma/data/kuma.db-wal /docker/uptime-kuma/data/kuma.db-shm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt; -d /tmp/kuma/upload &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cp -a /tmp/kuma/upload/. /docker/uptime-kuma/data/upload/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose start uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ntfy is the same dance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose stop ntfy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -xzf /root/ntfy-2026-07-16.tar.gz -C /tmp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /tmp/ntfy/user.db /docker/ntfy/var/lib/ntfy/user.db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -f /docker/ntfy/var/lib/ntfy/user.db-wal /docker/ntfy/var/lib/ntfy/user.db-shm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -a /tmp/ntfy/etc/. /docker/ntfy/etc/ntfy/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose start ntfy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Stop &lt;strong&gt;only&lt;/strong&gt; the affected service, never &lt;code&gt;docker compose down&lt;/code&gt;. The compose file is shared, and a &lt;code&gt;down&lt;/code&gt; takes both services out at once.&lt;/p&gt;
&lt;p&gt;Now verify, because an unverified restore is the same as no restore:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy user list &lt;span class="c1"&gt;# owner and publisher should both be there&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it ntfy ntfy access &lt;span class="c1"&gt;# per-topic write-only ACLs intact&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --fail -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &amp;lt;token&amp;gt;&amp;#34;&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;restore test&amp;#34;&lt;/span&gt; https://ntfy.example.com/KumaUptime &lt;span class="c1"&gt;# 200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For Kuma, open the SSH tunnel from earlier, log in with the restored admin account, and confirm your monitors are listed and the status page slug still reads &lt;code&gt;media&lt;/code&gt;. That slug is the one thing a restore can quietly get wrong. Rebuild Kuma from scratch with a different slug and the Caddy allowlist 404s the public page for everyone, while your dashboard looks perfect.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Status page loads but never updates.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A heartbeat API path is missing from the allowlist. Check the Caddy access log for 404s on &lt;code&gt;/api/status-page/heartbeat/&amp;lt;slug&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ntfy subscriptions hang or messages arrive in bursts.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing &lt;code&gt;flush_interval -1&lt;/code&gt; on the reverse proxy. Server-sent events are being buffered.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Publishes return 403 and nothing arrives.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;Authorization&lt;/code&gt; header is missing the &lt;code&gt;Bearer&lt;/code&gt; prefix, or the token&amp;rsquo;s ACL is scoped to a different topic than the one you&amp;rsquo;re publishing to.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Certificate issuance fails on the first deploy.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The DNS record doesn&amp;rsquo;t resolve yet. HTTP-01 can&amp;rsquo;t validate a name with no A record.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Port 3001 answers from outside despite a default-drop firewall.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The port is published on &lt;code&gt;0.0.0.0&lt;/code&gt;. Docker&amp;rsquo;s forwarding rules bypass the &lt;code&gt;INPUT&lt;/code&gt; chain. Fix the bind, not the firewall.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Message history vanished after a container update.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In-memory cache, working as configured. Set &lt;code&gt;NTFY_CACHE_FILE&lt;/code&gt; if you want history to survive restarts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A restored Kuma starts empty or throws SQLite errors.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leftover &lt;code&gt;kuma.db-wal&lt;/code&gt; and &lt;code&gt;kuma.db-shm&lt;/code&gt; from the previous database. Stop the container, delete both, start it again.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Caddy reload hangs after adding a new vhost.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mine hung for 30 minutes. Root cause: I ran &lt;code&gt;caddy validate&lt;/code&gt; as root. Validation &lt;em&gt;provisions&lt;/em&gt; the config, which created the new vhost&amp;rsquo;s access log owned &lt;code&gt;root:root 0600&lt;/code&gt;. The &lt;code&gt;caddy&lt;/code&gt; service user couldn&amp;rsquo;t open that file, the real reload never completed, and the systemd unit sat in a stale reloading state. Fix the ownership on &lt;code&gt;/var/log/caddy/&amp;lt;site&amp;gt;.access.log&lt;/code&gt;, then run &lt;code&gt;caddy reload --force&lt;/code&gt;. Validate as the &lt;code&gt;caddy&lt;/code&gt; user from now on.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A major Kuma version bump breaks the status page.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;That is the fail-closed allowlist behaving correctly. Re-audit the route list against the new release and add the paths the page now needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I really need a VPS? Can&amp;#39;t I use a second machine at home?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;A second box at home shares your power, your ISP, and your router. That&amp;rsquo;s the same failure domain, and it can&amp;rsquo;t report the outages that matter most. A VPS in another data center shares fate only with your public front door, which is exactly the isolation you&amp;rsquo;re paying for.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is a public status page a security risk?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Only in proportion to what you put on it. Use human-friendly monitor names instead of hostnames, and keep the monitor count small. My published page lists two services by friendly name and nothing else that resembles an inventory.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why ntfy instead of Discord, Telegram, or email?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;It&amp;rsquo;s self-hosted, so no third party sits in the alert path. Tokens are per-producer and individually revocable, ACLs can be write-only, and Uptime Kuma reaches it over the container network. Alerts still fire when DNS, Caddy, and the entire lab are down.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I keep ntfy private instead of exposing it publicly?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;You can, but then your phone only receives alerts while on your own network, which defeats the purpose. Publishing it behind Caddy with &lt;code&gt;deny-all&lt;/code&gt; auth and write-only producer tokens gives you off-network delivery without opening topics to anonymous readers. That combination is what makes public exposure defensible here.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What happens if my VPS provider has an outage?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Your monitoring goes dark. That&amp;rsquo;s a real limitation of any single-provider design. For most homelabs it&amp;rsquo;s an acceptable trade, since provider outages are rarer than lab outages. If uptime reporting is critical to you, add a second lightweight monitor with a different provider in a different region.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;Your phone gets the alert. Your users get a URL. Neither one depends on the lab being alive.&lt;/p&gt;
&lt;p&gt;The security posture, all of it: loopback binds instead of published ports, a Caddy allowlist that fails closed, an SSH tunnel for admin access, write-only per-producer tokens, and nothing on the public page that reads like an inventory.&lt;/p&gt;
&lt;p&gt;Monitoring hosted inside the thing it monitors is a nice dashboard. It isn&amp;rsquo;t a monitoring system.&lt;/p&gt;
&lt;p&gt;Next steps once this is running: point fail2ban at ntfy so SSH bans on the VPS push to your &lt;code&gt;Security&lt;/code&gt; topic, add your backup job outcomes as another producer, and put the status page link on the household dashboard so people find it before they find your phone number.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Uptime Kuma project and documentation: &lt;a class="link" href="https://github.com/louislam/uptime-kuma" target="_blank" rel="noopener"
&gt;https://github.com/louislam/uptime-kuma&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ntfy documentation (auth, ACLs, tokens, reverse proxy notes): &lt;a class="link" href="https://docs.ntfy.sh/" target="_blank" rel="noopener"
&gt;https://docs.ntfy.sh/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Caddy documentation (matchers, &lt;code&gt;handle&lt;/code&gt;, &lt;code&gt;reverse_proxy&lt;/code&gt;, automatic HTTPS): &lt;a class="link" href="https://caddyserver.com/docs/" target="_blank" rel="noopener"
&gt;https://caddyserver.com/docs/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Docker Compose file reference (port publishing and bind addresses): &lt;a class="link" href="https://docs.docker.com/reference/compose-file/" target="_blank" rel="noopener"
&gt;https://docs.docker.com/reference/compose-file/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>NPM to Caddy: Reverse Proxy Dozens of Containers</title><link>https://diymediaserver.com/post/2026/nginx-proxy-manager-to-caddy-migration/</link><pubDate>Fri, 03 Jul 2026 08:44:43 -0600</pubDate><guid>https://diymediaserver.com/post/2026/nginx-proxy-manager-to-caddy-migration/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/nginx-proxy-manager-to-caddy-migration/featured_hu_92d73671e0e975d0.webp" alt="Featured image of post NPM to Caddy: Reverse Proxy Dozens of Containers" /&gt;&lt;p&gt;8 years ago Nginx Proxy Manager was the right call. It was the first way I ever learned how to set up a reverse proxy on my home network without touching a line of Nginx syntax. Point at a container, tick a box for a Let&amp;rsquo;s Encrypt cert, and ten minutes later &lt;code&gt;sonarr.example.com&lt;/code&gt; resolved with a padlock. For the first handful of services it felt like magic.&lt;/p&gt;
&lt;p&gt;Several years later, at 31 services, it had become too much to manage through a web UI.&lt;/p&gt;
&lt;p&gt;I wanted to add one security header across every proxy host, and I realized that meant opening the same web form 31 times. That was the day I went looking for an effective way to reverse proxy many services without a database standing between me and my own config. The answer was Caddy. This post is the honest case for switching, the actual migration path I took, and the Caddy patterns that make 31 subdomains manageable from a terminal.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Nginx Proxy Manager is a great first reverse proxy, but its web UI and SQLite backend become a liability once you run dozens of internal services. Caddy replaces all of it with one text file plus wildcard TLS via DNS-01.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Proxmox VE&lt;/dt&gt;&lt;dd&gt;9.2.3&lt;/dd&gt;&lt;dt&gt;Caddy&lt;/dt&gt;&lt;dd&gt;2.11.4 (add-package &amp;#43; cloudflare-dns)&lt;/dd&gt;&lt;dt&gt;Cert&lt;/dt&gt;&lt;dd&gt;Cloudflare DNS-01 wildcard&lt;/dd&gt;&lt;dt&gt;Lxc&lt;/dt&gt;&lt;dd&gt;Debian 13 (unprivileged)&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-07-03&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I ran this migration on Caddy 2.11.4 with the Cloudflare DNS module added via &lt;code&gt;caddy add-package&lt;/code&gt;, running in an unprivileged Debian 13 LXC on Proxmox 9.2.3. Every config block below is lifted from the Caddyfile that now manages subdomains and TLS for my whole lab, with the domain and email swapped for placeholders.&lt;/p&gt;
&lt;h2 id="what-nginx-proxy-manager-gets-right"&gt;What Nginx Proxy Manager Gets Right
&lt;/h2&gt;&lt;p&gt;Before we go too far, I want to give credit where it&amp;rsquo;s due. NPM is the best on-ramp to reverse proxying that exists, and if you&amp;rsquo;re happy with it, nothing here should push you off it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It teaches the concepts without the syntax.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Proxy host, upstream, forward port, SSL cert. NPM&amp;rsquo;s UI makes those ideas concrete before you ever have to write an Nginx &lt;code&gt;location&lt;/code&gt; block.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificates are automatic and visible.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;You request a Let&amp;rsquo;s Encrypt cert from a dropdown, and the renewal status sits right there in the interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is genuinely fast to start.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Three to five services, a couple of subdomains, no need for wildcard certs or bulk edits. NPM does that job in ten minutes and never gets in your way.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I outgrew NPM. That&amp;rsquo;s a different statement from &amp;ldquo;NPM is bad.&amp;rdquo; If you run a small lab and prefer a GUI to a text file, stay there. The pain I&amp;rsquo;m about to describe only shows up at scale.&lt;/p&gt;
&lt;h2 id="where-it-started-to-hurt"&gt;Where It Started to Hurt
&lt;/h2&gt;&lt;p&gt;The 31-form marathon was the last straw. What made me leave was everything underneath it.&lt;/p&gt;
&lt;p&gt;Start with backups. The entire proxy layer for my homelab lived inside a SQLite database. I couldn&amp;rsquo;t read the config as a text file. Nor could I restore a single host. My backup plan amounted to copying the volume and praying, and a rebuild meant restoring a blob of data and hoping the current version still liked the format.&lt;/p&gt;
&lt;p&gt;Then there is the moving-parts problem. To write a proxy config that I would never get to read, NPM runs an Nginx instance, a Node.js admin app, and a SQLite database file underneath both. That&amp;rsquo;s a lot of machinery stacked up to manage text I have no direct access to. As someone who values fewer moving parts, that math stopped adding up.&lt;/p&gt;
&lt;p&gt;And there&amp;rsquo;s no escape from the web UI. There&amp;rsquo;s no way to bulk edit or find-and-replace. Every change is you, the mouse, and a browser form. None of this is a knock on NPM. It&amp;rsquo;s the difference between a tool built for point-and-click simplicity and a lab that has grown into wanting a simple way to manage a reverse proxy.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;The Intel NUC 12 Pro&lt;/strong&gt;
A compact &amp;quot;Wall Street Canyon&amp;quot; mini PC with a 12th-gen Core i5-1240P and Iris Xe that can drive up to four displays (dual Thunderbolt 4 + dual HDMI), plus 2.5GbE and Wi-Fi 6E. The H-chassis adds a 2.5″ bay alongside NVMe storage and up to 64GB RAM, making it a quiet, versatile homelab node or HTPC/office box.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-caddy-way-one-text-file"&gt;The Caddy Way: One Text File
&lt;/h2&gt;&lt;p&gt;The core of this whole migration fits in one sentence. My entire proxy layer is now a single Caddyfile that I can grep, diff, and edit over SSH in seconds.&lt;/p&gt;
&lt;p&gt;That single change fixes everything that was a pain above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bulk changes take one edit.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Change a shared snippet once and every site that imports it updates. The 31-form marathon becomes a one-line change.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups are a file.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;No database dump, no volume snapshot alignment. The Caddyfile is the backup.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can dry-run before you apply.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;caddy validate --config /etc/caddy/Caddyfile&lt;/code&gt; catches syntax errors before they hit production. The web UI never gave me a preview.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thing that makes this scale is snippets and &lt;code&gt;import&lt;/code&gt;. In Caddy, a snippet is a named block of config you define once and pull into any site. Here is the pattern that lets a host config fit on one screen. My standard HTTP upstream is one snippet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Standard HTTP upstream. reverse_proxy passes the Host header through by
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# default, so no header_up Host is needed.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;(proxy)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="se"&gt;{args[0]}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{client_ip}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that defined, every ordinary service becomes a three-line block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;sonarr.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;import&lt;/span&gt; proxy &lt;span class="n"&gt;http://192.168.1.13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;radarr.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;import&lt;/span&gt; proxy &lt;span class="n"&gt;http://192.168.1.13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those upstreams are Docker containers on another host. This is what a Caddy proxy looks like when the proxy itself lives outside Docker: you point &lt;code&gt;reverse_proxy&lt;/code&gt; at the container&amp;rsquo;s IP and port, and the container never has to know Caddy exists. Compare that to the NPM equivalent: two proxy hosts, each requiring a domain field, a scheme dropdown, a forward hostname, a forward port, an SSL tab, and a websocket checkbox, all entered by hand through a browser. The Caddyfile version is &lt;code&gt;git commit&lt;/code&gt;-ready and readable at a glance.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DNF5JWB4"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-5_hu_221d2b10d350a1b8.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-5_hu_5b2e6d1d98e59035.webp" width="600" height="461" alt="Raspberry Pi 5 (8GB) kit" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;This Raspberry Pi 5 (8GB) kit&lt;/strong&gt; bundles the board with a case, an active cooler, and a screwdriver, which covers the cooling and enclosure you&amp;rsquo;d otherwise source separately. The board is the biggest generational leap in Pi history. A quad-core Cortex-A76 at 2.4 GHz gives it roughly twice the CPU performance of a Pi 4, with PCIe 2.0 via the new FFC connector for NVMe storage, a real power button, and a dedicated Raspberry Pi RP1 I/O chip. Still sips power and fits in your hand, but now handles heavier Docker workloads, Pi-hole plus Home Assistant simultaneously, and even light Jellyfin hosting duties.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4w9N5xn" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-setup-caddy-in-a-proxmox-lxc-with-a-wildcard-cert"&gt;The Setup: Caddy in a Proxmox LXC with a Wildcard Cert
&lt;/h2&gt;&lt;p&gt;I run Caddy as a plain binary in a dedicated Proxmox LXC, not in Docker. The reasoning is deliberate. The proxy is infrastructure, so I want to run it with the fewest possible moving parts. An LXC with an apt-installed binary is trivially snapshotted by Proxmox, and there&amp;rsquo;s no Docker layer wrapping the one service that everything else depends on.&lt;/p&gt;
&lt;h3 id="create-the-container-and-install-caddy"&gt;Create the container and install Caddy
&lt;/h3&gt;&lt;p&gt;Spin up an unprivileged Debian 13 LXC, give it a static IP on your LAN, and make sure ports 80 and 443 are reachable. Then install Caddy from the &lt;a class="link" href="https://caddyserver.com/docs/install" target="_blank" rel="noopener"
&gt;official repository&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -1sLf &lt;span class="s1"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -1sLf &lt;span class="s1"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/caddy-stable.list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That gets you a running Caddy with a systemd config already enabled and started. Confirm it with &lt;code&gt;systemctl status caddy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is one wrinkle that trips up almost everyone. The stock Caddy binary doesn&amp;rsquo;t include DNS provider modules. To solve TLS challenges through Cloudflare DNS (the DNS-01 method), you need a build that includes the &lt;a class="link" href="https://github.com/caddy-dns/cloudflare" target="_blank" rel="noopener"
&gt;caddy-dns/cloudflare&lt;/a&gt; module. Every guide on the internet will tell you to compile one with xcaddy and a Go toolchain. You don&amp;rsquo;t need any of that. Caddy can replace its own binary with one that has the module baked in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo caddy add-package github.com/caddy-dns/cloudflare
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That one command grabs the latest Caddy release with the Cloudflare module baked in and swaps it into place at &lt;code&gt;/usr/bin/caddy&lt;/code&gt;. Restart the service, then confirm the module is present with &lt;code&gt;caddy list-modules | grep cloudflare&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a catch, and it&amp;rsquo;s worth fixing now rather than discovering it at renewal time. The binary on disk no longer matches what apt installed, so the next &lt;code&gt;apt upgrade&lt;/code&gt; of the caddy package quietly puts the stock module-less binary back, and certificate renewals start failing weeks later. The fix is to keep the package for its systemd unit, caddy user, and default config, but take the binary out of apt&amp;rsquo;s hands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt-mark hold caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A held package is never upgraded by apt. From now on, updates come from Caddy itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo caddy upgrade
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;caddy upgrade&lt;/code&gt; replaces the binary with the latest release &lt;em&gt;with the same modules installed&lt;/em&gt;, so the Cloudflare module survives every update, which is exactly the promise apt couldn&amp;rsquo;t make. The tradeoff is that Caddy updates are now your job instead of unattended-upgrades&amp;rsquo; job, so check &lt;code&gt;caddy version&lt;/code&gt; against the &lt;a class="link" href="https://github.com/caddyserver/caddy/releases" target="_blank" rel="noopener"
&gt;releases page&lt;/a&gt; once in a while. If you ever want apt back in charge, &lt;code&gt;sudo apt-mark unhold caddy&lt;/code&gt; reverses it.&lt;/p&gt;
&lt;h3 id="wildcard-tls-via-dns-01"&gt;Wildcard TLS via DNS-01
&lt;/h3&gt;&lt;p&gt;This is the piece that makes many internal subdomains painless. With a DNS-01 challenge, Caddy proves it owns a name by writing a temporary TXT record through the Cloudflare API, so nothing has to listen on port 80 to the public internet. That matters for LAN-only services, because Let&amp;rsquo;s Encrypt could never reach them over HTTP-01 anyway.&lt;/p&gt;
&lt;p&gt;Left alone, Caddy would request a separate certificate for every site block that way. I don&amp;rsquo;t let it, for a privacy reason that is easy to miss: every certificate ever issued is published to public Certificate Transparency logs, so per-subdomain certs put the name of each internal service into a searchable public index (look your domain up on &lt;a class="link" href="https://crt.sh" target="_blank" rel="noopener"
&gt;crt.sh&lt;/a&gt; sometime). One wildcard certificate for &lt;code&gt;*.example.com&lt;/code&gt; avoids that. The logs show the wildcard entry and nothing else, and outsiders learn nothing about what actually runs in the lab.&lt;/p&gt;
&lt;p&gt;Since Caddy 2.10, getting the wildcard takes no special option. If the Caddyfile contains a wildcard site block, every subdomain site it covers automatically shares that block&amp;rsquo;s certificate instead of requesting its own. So this short block sits near the top of my Caddyfile:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Holds the *.example.com wildcard cert; every subdomain block shares it.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Also catches requests for subdomains that don&amp;#39;t exist.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;*.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;abort&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;abort&lt;/code&gt; means any subdomain you never defined gets its connection closed instead of a blank page. And don&amp;rsquo;t delete this block once you rely on it: remove it and Caddy quietly goes back to issuing individual certs, and your hostnames start leaking into CT logs again. (On Caddy 2.8 and 2.9 the same behavior needed the &lt;code&gt;auto_https prefer_wildcard&lt;/code&gt; global option; 2.10 made it the default and removed the option.)&lt;/p&gt;
&lt;p&gt;The other half of the story is plain DNS. Certificates prove who a host is; something still has to point &lt;code&gt;sonarr.example.com&lt;/code&gt; at the proxy in the first place. That happens entirely inside the LAN: add one wildcard record (&lt;code&gt;*.example.com&lt;/code&gt; pointing at the Caddy LXC&amp;rsquo;s IP) to your local resolver, Pi-hole in my case, and every subdomain resolves to Caddy without you ever touching DNS again. No public A records are needed at all. DNS-01 only ever creates that short-lived TXT record at Cloudflare, so the public internet never learns where your hosts live.&lt;/p&gt;
&lt;p&gt;First, create a scoped Cloudflare API token. In the Cloudflare dashboard, go to My Profile, then API Tokens, and create a token with &lt;code&gt;Zone&lt;/code&gt; &amp;gt; &lt;code&gt;DNS&lt;/code&gt; &amp;gt; &lt;code&gt;Edit&lt;/code&gt; permission limited to the zone you use for the lab. Copy the token, because Cloudflare only shows it once.&lt;/p&gt;
&lt;p&gt;Store it where Caddy&amp;rsquo;s systemd unit can read it, out of the Caddyfile itself, in &lt;code&gt;/etc/caddy/caddy.env&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-scoped-token-here
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That file holds a live credential, so lock it down: &lt;code&gt;sudo chmod 600 /etc/caddy/caddy.env&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then point Caddy&amp;rsquo;s global options block at it. This block is set once and forgotten:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;email&lt;/span&gt; &lt;span class="s"&gt;admin@example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;acme_dns&lt;/span&gt; &lt;span class="s"&gt;cloudflare&lt;/span&gt; &lt;span class="se"&gt;{env.CLOUDFLARE_API_TOKEN}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;servers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;trusted_proxies&lt;/span&gt; &lt;span class="s"&gt;static&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="s"&gt;.10.10.0/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;email&lt;/code&gt; line is your ACME account contact. &lt;code&gt;acme_dns cloudflare&lt;/code&gt; tells Caddy to solve every challenge through Cloudflare DNS, which is what unlocks real certificates for LAN-only hosts. The &lt;code&gt;servers&lt;/code&gt; block is only there because another proxy sits in front of this one for external traffic: my &lt;a class="link" href="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/" &gt;WireGuard VPS tunnel&lt;/a&gt; forwards public requests over the tunnel subnet, and &lt;code&gt;trusted_proxies&lt;/code&gt; tells Caddy to believe the client IP headers that edge proxy sends instead of logging every external visitor as the tunnel address. If nothing forwards traffic to your Caddy, drop the &lt;code&gt;servers&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;{env.CLOUDFLARE_API_TOKEN}&lt;/code&gt; placeholder only works if the token is in Caddy&amp;rsquo;s environment when systemd starts the service, which means the unit needs an &lt;code&gt;EnvironmentFile=&lt;/code&gt; line pointing at &lt;code&gt;/etc/caddy/caddy.env&lt;/code&gt;. Do not add it by editing the packaged unit file directly. The next &lt;code&gt;apt upgrade&lt;/code&gt; of the caddy package will replace &lt;code&gt;/lib/systemd/system/caddy.service&lt;/code&gt; and silently throw your change away. The clean way is a systemd drop-in: a small override file in &lt;code&gt;/etc/systemd/system/caddy.service.d/&lt;/code&gt; that systemd merges on top of the packaged unit, and that survives package upgrades untouched. Mine lives at &lt;code&gt;/etc/systemd/system/caddy.service.d/env.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /etc/systemd/system/caddy.service.d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tee /etc/systemd/system/caddy.service.d/env.conf &amp;gt; /dev/null &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;[Service]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EnvironmentFile=/etc/caddy/caddy.env
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm the token actually made it into the service with &lt;code&gt;systemctl show caddy | grep CLOUDFLARE&lt;/code&gt;.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
This mini workstation built around a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. It is ideal for running Proxmox and hosting Caddy as a reverse proxy in a homelab, offering high core counts and robust networking for managing many internal subdomains.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-migration-itself"&gt;The Migration Itself
&lt;/h2&gt;&lt;p&gt;The mapping from NPM to Caddy isn&amp;rsquo;t too bad once you see it. An NPM proxy host becomes a Caddy site block. The forward hostname and port become the argument to &lt;code&gt;reverse_proxy&lt;/code&gt;. Any custom Nginx snippet you bolted on becomes a named directive inside the block.&lt;/p&gt;
&lt;p&gt;Before writing any site blocks, get the full host list out of NPM. The config that lived in a database you could never read is one query away from becoming a migration checklist. The file is &lt;code&gt;database.sqlite&lt;/code&gt; in NPM&amp;rsquo;s data volume:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sqlite3 database.sqlite &lt;span class="s2"&gt;&amp;#34;SELECT domain_names, forward_scheme, forward_host, forward_port FROM proxy_host WHERE is_deleted = 0;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every row is a site block you are about to write: domain, scheme, upstream IP, and port. Mine came out to 31 lines, and that printout became the cutover checklist.&lt;/p&gt;
&lt;p&gt;The order that avoids downtime matters. Here is what I did, and it kept the lab online the whole way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stand up Caddy in parallel&lt;/strong&gt; on its own IP while NPM keeps running untouched.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move subdomains over in batches&lt;/strong&gt; by repointing internal DNS one service at a time. Test each one before moving the next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep NPM as the fallback&lt;/strong&gt; until zero hosts point at it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decommission NPM&lt;/strong&gt; and, if you like, hand its old IP to the Caddy container so nothing downstream notices.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
If you change Caddy&amp;rsquo;s IP make sure you also change the DNS entries.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Per service, the cutover felt trivial: type a four-line block and run &lt;code&gt;caddy reload&lt;/code&gt;, and refresh the browser.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t bother revoking NPM&amp;rsquo;s old Let&amp;rsquo;s Encrypt certificates. Once your Caddy wildcard cert is issued and serving traffic, the NPM certs are dead weight. Leave them to expire on their own. There is no security benefit to revoking a cert nobody is presenting anymore, and revocation is one more manual step at the exact moment you want to be decommissioning, not fiddling about.&lt;/p&gt;
&lt;h3 id="the-gotchas-i-hit"&gt;The gotchas I hit
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Websockets.&lt;/strong&gt; In NPM this was a checkbox you had to remember to tick. In Caddy, &lt;code&gt;reverse_proxy&lt;/code&gt; upgrades websockets automatically, so most apps work with no extra directives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access Lists.&lt;/strong&gt; If you leaned on NPM&amp;rsquo;s Access Lists, both halves have direct Caddy equivalents: an IP allowlist becomes a &lt;code&gt;remote_ip&lt;/code&gt; matcher (&lt;code&gt;@outside not remote_ip 192.168.1.0/24&lt;/code&gt; followed by &lt;code&gt;abort @outside&lt;/code&gt;), and basic auth becomes the &lt;code&gt;basic_auth&lt;/code&gt; directive fed a hash from &lt;code&gt;caddy hash-password&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming and server-sent events.&lt;/strong&gt; A few apps (ntfy, my search backend) buffer badly behind a default proxy. Caddy fixes it with &lt;code&gt;flush_interval -1&lt;/code&gt; to disable response buffering. I keep that in a separate snippet:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Streaming upstream: disable response buffering.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;(proxy_flush)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="se"&gt;{args[0]}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{client_ip}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;flush_interval&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTPS upstreams like Proxmox and PBS.&lt;/strong&gt; Most services behind the proxy speak plain HTTP, but Proxmox (port 8006) and Proxmox Backup Server (8007) terminate their own TLS, so Caddy has to make an HTTPS connection to the backend instead. The catch is that my Caddyfile points at them by IP address. During the TLS handshake, Caddy would send that IP as the server name (SNI), the upstream would answer with a certificate issued for its hostname, and Caddy would reject the mismatch with an error like &lt;code&gt;certificate is valid for pve01.example.com, not 192.168.1.2&lt;/code&gt;. The fix is &lt;code&gt;tls_server_name&lt;/code&gt;, which tells Caddy which hostname to send as SNI and to verify the upstream&amp;rsquo;s certificate against. In this snippet that hostname comes in as the second argument:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# HTTPS upstream with a real ACME cert (Proxmox, PBS). Caddy calls by IP, so
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# args[1] supplies the cert hostname for SNI + verification.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;(proxy_tls)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="se"&gt;{args[0]}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{client_ip}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transport&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;tls_server_name&lt;/span&gt; &lt;span class="se"&gt;{args[1]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A Proxmox node then reduces to &lt;code&gt;import proxy_tls https://192.168.1.2:8006 pve01.example.com&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One stubborn upstream.&lt;/strong&gt; My OPNsense box runs lighttpd, which negotiates HTTP/2 via ALPN, and HTTP/2 cannot carry websocket upgrades. I had to pin that single hop to HTTP/1.1 with &lt;code&gt;versions 1.1&lt;/code&gt; inside its transport block. That&amp;rsquo;s the kind of one-off you write inline rather than in a shared snippet:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# OPNsense: lighttpd negotiates HTTP/2 over ALPN, and HTTP/2 cannot carry
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# websocket upgrades. Pin this one hop to HTTP/1.1.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;opnsense.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;https://192.168.1.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{client_ip}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transport&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;tls_server_name&lt;/span&gt; &lt;span class="s"&gt;opnsense.example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;versions&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="advanced-topics"&gt;Advanced Topics
&lt;/h2&gt;&lt;p&gt;Once the basics work, Caddy has depth that NPM never exposed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The admin API.&lt;/strong&gt; Caddy ships a JSON admin API on &lt;code&gt;localhost:2019&lt;/code&gt;.&lt;/p&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
Read this before you touch it: this API can replace your entire running configuration in less than a second. Not one host. All of it. Anyone who can reach &lt;code&gt;localhost:2019&lt;/code&gt; can wipe or rewrite every proxy you run, so it must never be exposed to the network as-is.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The saving grace is that a stock install binds the admin endpoint to &lt;code&gt;localhost:2019&lt;/code&gt; only, so nothing on your network can reach it until you deliberately publish it. If you need a piece of it, for example so a dashboard can poll upstream health, publish only the read-only path and lock it to your LAN:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;:2020&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@upstreams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;path&lt;/span&gt; &lt;span class="s"&gt;/reverse_proxy/upstreams&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;remote_ip&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="s"&gt;.168.1.0/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;handle&lt;/span&gt; &lt;span class="nd"&gt;@upstreams&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2019&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2019&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;handle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;respond&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That block publishes only &lt;code&gt;/reverse_proxy/upstreams&lt;/code&gt; to the LAN and returns 403 for everything else. The full config-replacement endpoints on &lt;code&gt;2019&lt;/code&gt; stay bound to localhost, unreachable from the network. Copy this pattern exactly. Do not widen the &lt;code&gt;path&lt;/code&gt; matcher to the whole API &amp;ldquo;for convenience,&amp;rdquo; because that convenience is a remote config wipe waiting to happen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Matchers for traffic control.&lt;/strong&gt; The &lt;code&gt;@upstreams&lt;/code&gt; name above is a matcher. Matchers route by path, header, source IP, or method. Send one URL prefix to one backend and everything else to another, all inside a single site block.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Custom headers and security.&lt;/strong&gt; Shared security headers belong in a snippet you import everywhere, so a policy change is a one-line edit across all 31 hosts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;(security_headers)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;max-age=31536000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;SAMEORIGIN&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nosniff&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add &lt;code&gt;import security_headers&lt;/code&gt; next to the &lt;code&gt;import proxy&lt;/code&gt; line in each site block. One caution before you copy that: HSTS orders browsers to refuse plain HTTP for the whole &lt;code&gt;max-age&lt;/code&gt; window, so a broken cert locks you out of any HTTP fallback until it&amp;rsquo;s fixed. Use a short &lt;code&gt;max-age&lt;/code&gt; like &lt;code&gt;3600&lt;/code&gt; while testing and raise it once everything is stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logs.&lt;/strong&gt; NPM showed per-host access logs in its UI. Caddy logs to the systemd journal by default, and adding a &lt;code&gt;log&lt;/code&gt; directive inside any site block turns on structured access logs for that host. Put it in a shared snippet if you want it everywhere, and read the results with &lt;code&gt;journalctl -u caddy&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;caddy reload&lt;/code&gt; fails.&lt;/strong&gt; Run &lt;code&gt;caddy validate --config /etc/caddy/Caddyfile&lt;/code&gt; first. It reports the exact line for missing snippet names, unbalanced braces, or a bad directive before anything goes live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The wildcard cert never issues, ACME DNS errors in the log.&lt;/strong&gt; This is almost always the Cloudflare token. Confirm the module is compiled in with &lt;code&gt;caddy list-modules | grep cloudflare&lt;/code&gt;, verify the token has &lt;code&gt;Zone:DNS:Edit&lt;/code&gt; on the right zone, and check that the environment variable is actually loaded (&lt;code&gt;systemctl show caddy | grep CLOUDFLARE&lt;/code&gt;). If you are stuck, temporarily drop &lt;code&gt;acme_dns&lt;/code&gt; and test one public host over HTTP-01 to isolate whether the break is DNS or networking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issuance hangs at &amp;ldquo;waiting for DNS propagation&amp;rdquo;.&lt;/strong&gt; This one bites homelabs specifically. Before asking Let&amp;rsquo;s Encrypt to validate, Caddy checks that the TXT record is visible, and it does that through the system resolver. If your LXC&amp;rsquo;s resolver is your own split-horizon DNS, the one that answers for the lab domain locally, that check may never see the public record. Point the challenge at a public resolver by swapping the &lt;code&gt;acme_dns&lt;/code&gt; one-liner for the fuller issuer form:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddyfile" data-lang="caddyfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;email&lt;/span&gt; &lt;span class="s"&gt;admin@example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;cert_issuer&lt;/span&gt; &lt;span class="s"&gt;acme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;dns&lt;/span&gt; &lt;span class="s"&gt;cloudflare&lt;/span&gt; &lt;span class="se"&gt;{env.CLOUDFLARE_API_TOKEN}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;resolvers&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1.1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proxy works from outside but not inside, or the reverse.&lt;/strong&gt; This is a DNS resolution mismatch. Internal subdomains need consistent resolution, so point your local resolver (or split-horizon DNS) at the Caddy IP for the wildcard record.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App loads but shows the wrong client IP.&lt;/strong&gt; If clients reach Caddy directly, the fix is the &lt;code&gt;header_up X-Real-IP {client_ip}&lt;/code&gt; line in the snippet the site imports. If another proxy forwards traffic to Caddy, as my VPS edge does, its subnet must be listed under &lt;code&gt;trusted_proxies&lt;/code&gt; in the global block, or Caddy ignores the forwarded headers and every visitor shows up as the proxy&amp;rsquo;s own IP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A service loses websockets after cutover.&lt;/strong&gt; Confirm the upstream is not being forced to HTTP/2. Pin that hop to &lt;code&gt;versions 1.1&lt;/code&gt; inside the &lt;code&gt;transport http&lt;/code&gt; block, as with the OPNsense example above.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is Caddy harder to learn than Nginx Proxy Manager?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The first site block is harder than clicking through NPM, because you write text instead of filling a form. After that Caddy is easier. Once you have a working snippet, adding a service is a three-line block you copy and edit.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need to open port 80 or 443 to the internet for certificates?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. With a DNS-01 challenge, Caddy proves domain ownership through your DNS provider&amp;rsquo;s API instead of an inbound HTTP request. That means you can issue real, trusted certificates for LAN-only services that the public internet can never reach.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why Caddy and not Traefik for many Docker services?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Traefik&amp;rsquo;s strength is Docker label discovery, which fits an all-Docker host well. My lab is mixed LXC, VM, and Docker, so a central Caddyfile with explicit upstreams is cleaner for me than labels scattered across hosts. Both are solid; the model fit my layout better.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I manage the Caddyfile in git and reload safely?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, and this is the main payoff. Commit the Caddyfile to a private repo, run &lt;code&gt;caddy validate&lt;/code&gt; to catch errors, then &lt;code&gt;caddy reload&lt;/code&gt; to apply with zero downtime. Every change is diffable and revertible, which no GUI-driven proxy gives you.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does a reverse proxy make my services secure?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not on its own. A reverse proxy is not a firewall, despite how often it gets framed that way on Reddit. It terminates TLS and routes traffic. Real security is defense in depth: a firewall, network segmentation, authentication, and keeping services patched.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway
&lt;/h2&gt;&lt;p&gt;The switch had nothing to do with performance or features. Both proxies serve pages fine, and neither one is going to bottleneck a homelab. What changed was ownership of the config. It went from a database I had to visit through a browser to a file I own and can read.&lt;/p&gt;
&lt;p&gt;At five services, NPM&amp;rsquo;s web UI is a convenience. A few dozen or more and it becomes a liability. Caddy gave me the most effective way to reverse proxy many Docker and LXC services on one system: bulk edits in seconds, config tracked in git, backups that are one file, and rebuilds that are one paste into a new LXC. If you&amp;rsquo;re running a public edge as well, Caddy fits there too. My &lt;a class="link" href="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/" &gt;WireGuard VPS tunnel&lt;/a&gt; already uses Caddy at the VPS end, so the internal and external proxies now speak the same language.&lt;/p&gt;
&lt;p&gt;One last thing worth repeating. A reverse proxy is not a security product, no matter how often that claim gets made online. It routes traffic and handles certificates. Keep a real firewall in front of it, segment your network, and treat the proxy as one layer among several. Defense in depth is the only honest answer.&lt;/p&gt;
&lt;p&gt;If you want a starting point, copy the global block, the wildcard block, the &lt;code&gt;(proxy)&lt;/code&gt; snippet, and three example service blocks from above into &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;, swap in your own domain and upstream IPs, run &lt;code&gt;caddy validate&lt;/code&gt;, then &lt;code&gt;caddy reload&lt;/code&gt;. That&amp;rsquo;s the whole workflow, and it stays the same whether you have three services or 31.&lt;/p&gt;</description></item><item><title>Arr Stack Docker Compose: Full Usenet Setup Guide on Proxmox</title><link>https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/</link><pubDate>Fri, 12 Jun 2026 05:51:42 -0600</pubDate><guid>https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_54f90d78479f24f2.webp" alt="Featured image of post Arr Stack Docker Compose: Full Usenet Setup Guide on Proxmox" /&gt;&lt;p&gt;I initially built my *arr apps in separate LXC containers, and the result was a mess. The volume paths were hard to manage and never quite lined up right, and Radarr and Sonarr kept failing to import movies and shows because of permission issues. Every &amp;ldquo;Import failed, path does not exist&amp;rdquo; error sent me back to the settings page. So I tore the whole thing down and rebuilt it as one Docker Compose file, with a shared bridge network. That was the day automation finally worked. This post is the &lt;strong&gt;arr stack docker compose&lt;/strong&gt; guide I wish I&amp;rsquo;d had.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been searching for a walkthrough that covers the whole suite at once instead of four separate install posts, this is it. We&amp;rsquo;ll stand up Prowlarr, Radarr, Sonarr, Bazarr, and SABnzbd in a single Compose file on a Debian 13 VM in Proxmox, and we&amp;rsquo;ll set up Prowlarr as the central indexer hub so you only have to configure your indexers once.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;Usenet&lt;/strong&gt; guide, not a torrent guide. I don&amp;rsquo;t seed, so there&amp;rsquo;s no ratio to protect and no need to keep a second copy of every file hanging around. My downloads go to a dedicated NVMe drive where SABnzbd does its repair and unpack work, and the finished media is then written over to my mergerfs NAS pool. The download drive and the media pool are deliberately two different filesystems, and as you&amp;rsquo;ll see, that&amp;rsquo;s completely fine for a Usenet setup. If you run torrents and care about seeding from the same files you import, this is not the best guide for you.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Run the entire *arr suite plus SABnzbd from one Docker Compose file on a Debian VM, download to a fast NVMe drive, and let the apps import into a mergerfs media pool. Prowlarr syncs your Usenet indexers into Radarr and Sonarr automatically.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Proxmox VE&lt;/dt&gt;&lt;dd&gt;9.3.2&lt;/dd&gt;&lt;dt&gt;Docker&lt;/dt&gt;&lt;dd&gt;29.6.0&lt;/dd&gt;&lt;dt&gt;Sonarr&lt;/dt&gt;&lt;dd&gt;4.0.17&lt;/dd&gt;&lt;dt&gt;Radarr&lt;/dt&gt;&lt;dd&gt;6.2.1&lt;/dd&gt;&lt;dt&gt;Prowlarr&lt;/dt&gt;&lt;dd&gt;2.4.0&lt;/dd&gt;&lt;dt&gt;Bazarr&lt;/dt&gt;&lt;dd&gt;1.5.6&lt;/dd&gt;&lt;dt&gt;SABnzbd&lt;/dt&gt;&lt;dd&gt;5.0.4&lt;/dd&gt;&lt;dt&gt;Vm os&lt;/dt&gt;&lt;dd&gt;Debian 13 (Trixie)&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-06-20&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I run this setup on Proxmox 9.3.2 inside a Debian 13 VM with Docker 29.6.0, using the LinuxServer.io images for Prowlarr 2.4.0, Radarr 6.2.1, Sonarr 4.0.17, and Bazarr 1.5.6, SABnzbd 5.0.4 image. Every command and path below is copied from this working stack.&lt;/p&gt;
&lt;h2 id="why-one-compose-file-beats-four-install-guides"&gt;Why One Compose File Beats Four Install Guides
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the biggest lesson from my initial setup: the *arr apps are a system, not a collection of independent programs. Radarr needs to reach SABnzbd. Prowlarr needs to push indexers into both Radarr and Sonarr. Bazarr needs to see the same media folders Radarr and Sonarr write to. When you install each one separately, you make a fresh networking and permissions decision every time, and those decisions drift out of sync.&lt;/p&gt;
&lt;p&gt;A single Docker Compose file fixes this at the root. One file defines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One shared user-defined bridge network, so every app can reach every other app by container name.&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; pair, so every container reads and writes files as the same user.&lt;/li&gt;
&lt;li&gt;One lifecycle: &lt;code&gt;docker compose up -d&lt;/code&gt; to start the whole arr suite, &lt;code&gt;docker compose down&lt;/code&gt; to stop it, and &lt;code&gt;docker compose pull&lt;/code&gt; to update.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compose centralizes networking, volumes, UIDs, and updates in a single source of truth.&lt;/p&gt;
&lt;h2 id="decision-first-vm-bare-metal-or-lxc"&gt;Decision First: VM, Bare Metal, or LXC
&lt;/h2&gt;&lt;p&gt;Before any commands, make the architecture choice, because it changes everything downstream. This guide is written around Proxmox, but nothing here is Proxmox-specific. The stack runs equally well on a dedicated bare-metal server. What you want, either way, is a normal Linux environment for Docker.&lt;/p&gt;
&lt;h3 id="use-a-vm-or-bare-metal-recommended"&gt;Use a VM or bare metal (recommended)
&lt;/h3&gt;&lt;p&gt;Create a Debian or Ubuntu VM, install Docker inside it, and run the whole stack there. A bare-metal Debian or Ubuntu box with Docker installed directly works exactly the same way, so if you have a spare machine and don&amp;rsquo;t need Proxmox, point it at that instead. Either path is what I run and what I recommend for almost everyone. Both give Docker a clean, normal Linux environment, so &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; behave exactly the way every LinuxServer.io guide assumes. No cgroup edits, no device passthrough wrestling, no surprise &lt;code&gt;nobody:nogroup&lt;/code&gt; ownership.&lt;/p&gt;
&lt;p&gt;Choose this if you want the least UID/GID and networking pain. On Proxmox the cost is a little more RAM overhead than an LXC; bare metal has none of that overhead at all.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has more than enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without turning your closet into a jet engine or space heater. The extra NVMe slots matter here, since Usenet leans hard on a fast scratch drive for repair and unpack.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="use-a-privileged-lxc-advanced-not-recommended"&gt;Use a privileged LXC (advanced, not recommended)
&lt;/h3&gt;&lt;p&gt;An LXC uses fewer resources, but to run Docker inside one on Proxmox it usually needs to be &lt;strong&gt;privileged&lt;/strong&gt;, and you have to edit &lt;code&gt;/etc/pve/lxc/&amp;lt;id&amp;gt;.conf&lt;/code&gt; to allow the devices and mounts Docker expects. Here&amp;rsquo;s the trap that bites almost everyone: an unprivileged LXC with host NFS/SMB storage often shows your media as owned by &lt;code&gt;nobody&lt;/code&gt;, which makes imports fail, and you need to understand how to map users and groups in the LXC config. I have guides on this, but it&amp;rsquo;s outside the scope of this post.&lt;/p&gt;
&lt;p&gt;Choose this only if you&amp;rsquo;re resource-constrained and comfortable debugging Proxmox LXC permissions quirks. A full VM, or a bare-metal box with Docker, is simpler and more reliable for the *arr suite, which is why I don&amp;rsquo;t recommend the LXC route for this stack. And because I&amp;rsquo;m weird, I only run unprivileged LXCs and never privileged ones. If something needs that level of permission, it gets a VM, not an LXC.&lt;/p&gt;
&lt;p&gt;The rest of this guide assumes the VM (or bare-metal) path. I&amp;rsquo;ll flag the LXC-specific steps where they differ.&lt;/p&gt;
&lt;h2 id="step-1-plan-the-storage-layout"&gt;Step 1: Plan the Storage Layout
&lt;/h2&gt;&lt;p&gt;This is the step that, done wrong, causes import failures down the line. So let me be clear about what this setup is doing, because it&amp;rsquo;s the opposite of the torrent-focused guides you&amp;rsquo;ve probably read.&lt;/p&gt;
&lt;p&gt;My download drive and my media library are two separate filesystems on purpose. Downloads land on a fast NVMe drive, where SABnzbd does the heavy, IO-intensive work of par2 repair and rar extraction. The finished files are then imported onto a mergerfs pool mounted at &lt;code&gt;/media/Storage&lt;/code&gt;, which is where the actual library lives. When Radarr or Sonarr import a file, it gets copied from the NVMe onto the pool.&lt;/p&gt;
&lt;p&gt;That copy is not a bug, and it&amp;rsquo;s not something to engineer away. For Usenet there&amp;rsquo;s nothing to seed, so there&amp;rsquo;s no reason to keep the downloaded file linked to the library file. The bytes have to physically move from the NVMe onto the pool to end up in the library no matter what, so a one-time copy on import is exactly the behavior you want. You get NVMe speed where it actually matters, during repair and unpack, and your bulk storage stays on the pool. The only cost is a brief moment of the file existing in two places during the copy, which the app cleans up afterward.&lt;/p&gt;
&lt;p&gt;This is also why you will not see a single &lt;code&gt;/data&lt;/code&gt; mount or any hardlink settings in this guide. Those exist to keep a seeding torrent and its imported copy as one set of bytes on one filesystem. With Usenet you don&amp;rsquo;t need that, so the layout is simpler.&lt;/p&gt;
&lt;p&gt;On the VM, create the download tree on the NVMe drive. Set &lt;code&gt;DOWNLOADS_PATH&lt;/code&gt; to wherever your NVMe is mounted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /downloads/&lt;span class="o"&gt;{&lt;/span&gt;incomplete,complete/&lt;span class="o"&gt;{&lt;/span&gt;movies,tv&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the media library on the mergerfs pool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /media/Storage/&lt;span class="o"&gt;{&lt;/span&gt;Movies,Shows,Hallmark&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I keep a separate &lt;code&gt;Hallmark&lt;/code&gt; library because Radarr handles it as its own root folder, which keeps those movies out of my main collection. You can drop that folder if you don&amp;rsquo;t need a second movie root.&lt;/p&gt;
&lt;p&gt;Finally, create a tree for container configuration. Keep it off &lt;code&gt;$HOME&lt;/code&gt; to avoid permission and backup headaches:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /docker/&lt;span class="o"&gt;{&lt;/span&gt;prowlarr,sonarr,radarr,bazarr,sabnzbd&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="set-ownership-and-capture-puidpgid"&gt;Set ownership and capture PUID/PGID
&lt;/h3&gt;&lt;p&gt;Pick a user that will own the media. I use a dedicated user &lt;code&gt;myuser&lt;/code&gt; who is a member of the &lt;code&gt;media&lt;/code&gt; group. Find its numeric IDs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id myuser
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That prints something like &lt;code&gt;uid=1000(myuser) gid=1001(media)&lt;/code&gt;. Those two numbers become your &lt;code&gt;PUID&lt;/code&gt; and &lt;code&gt;PGID&lt;/code&gt; in the &lt;code&gt;.env&lt;/code&gt; file, and they don&amp;rsquo;t have to match each other. Mine are &lt;code&gt;1000&lt;/code&gt; and &lt;code&gt;1001&lt;/code&gt;. Now hand the download and config folders to that user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R myuser:media /downloads /docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /downloads /docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Don&amp;rsquo;t let that &lt;code&gt;chmod&lt;/code&gt; string scare you. It gives the owner and the &lt;code&gt;media&lt;/code&gt; group full read and write, and traverse on directories, while locking everyone else out completely. That lines up with the &lt;code&gt;UMASK=0007&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt;, which tells the containers to create new files the same way: owner and group only, nothing for anyone else.&lt;/p&gt;
&lt;p&gt;The mergerfs pool is the one place to slow down. Because the pool sits on top of your NAS disks, its ownership is governed at the storage layer, not by a simple &lt;code&gt;chown&lt;/code&gt; on the mount. The thing that actually matters is that the &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; your containers run as can write to &lt;code&gt;/media/Storage&lt;/code&gt;. Confirm that before you go further, because a pool the apps can&amp;rsquo;t write to is the most common cause of silent import failures in this kind of split setup.&lt;/p&gt;
&lt;div class="alert alert-default"&gt;
&lt;span class="alert-icon"&gt;ℹ️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Info:&lt;/strong&gt;
Lessons learned: the user that owns your downloads and the user that can write to your media pool must both line up with the &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; the containers run as. Write the UID and GID down now, you&amp;rsquo;ll paste them into the &lt;code&gt;.env&lt;/code&gt; file in a minute.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Checkpoint: log in as &lt;code&gt;myuser&lt;/code&gt; and run &lt;code&gt;touch /downloads/complete/movies/test&lt;/code&gt; and &lt;code&gt;touch /media/Storage/Movies/test&lt;/code&gt;. No permission error on either means you&amp;rsquo;re good. Delete the test files afterward.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DX2DPJZ5"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NVME-2TB_hu_4dac0086f1363f66.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NVME-2TB_hu_fb4af511003be25d.webp" width="600" height="600" alt="Samsung NVMe 9100 PRO 2TB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
Samsung 9100 PRO 2TB is a high-performance NVMe SSD that delivers fast PCIe speeds and responsive everyday performance, making it an ideal SABnzbd scratch and processing drive.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3YMvopG" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-2-prepare-the-vm-and-install-docker"&gt;Step 2: Prepare the VM and Install Docker
&lt;/h2&gt;&lt;p&gt;Update the OS and install Docker using the official convenience script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://get.docker.com &lt;span class="p"&gt;|&lt;/span&gt; sudo sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -aG docker myuser
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Log out and back in as &lt;code&gt;myuser&lt;/code&gt; so Docker runs without &lt;code&gt;sudo&lt;/code&gt;. Verify:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run hello-world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the LXC path instead: create a privileged Debian/Ubuntu container, bind-mount your storage into it, then install Docker the same way. Be aware of the &lt;code&gt;nobody:nogroup&lt;/code&gt; trap mentioned earlier, which is the main reason I steer people toward a VM for this.&lt;/p&gt;
&lt;p&gt;Checkpoint: &lt;code&gt;docker info&lt;/code&gt; runs cleanly and &lt;code&gt;hello-world&lt;/code&gt; prints its success message.&lt;/p&gt;
&lt;h2 id="step-3-write-the-docker-compose-file"&gt;Step 3: Write the Docker Compose File
&lt;/h2&gt;&lt;p&gt;This is the heart of the &lt;strong&gt;arr stack docker compose&lt;/strong&gt; setup. First, a quick note on two design choices in my file.&lt;/p&gt;
&lt;p&gt;I keep every tunable value in a &lt;code&gt;.env&lt;/code&gt; file rather than hardcoding it in the Compose file. Ports, paths, and the user IDs all live in one place, which makes the Compose file readable and easy to reuse. I also run every service on an &lt;strong&gt;external&lt;/strong&gt; network called &lt;code&gt;media_network&lt;/code&gt;, which means the network is created once, outside of Compose, and the stack attaches to it. That keeps the network stable even when I tear the stack down and bring it back up.&lt;/p&gt;
&lt;p&gt;Create a project directory and the &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; .env &lt;span class="s"&gt;&amp;lt;&amp;lt;EOF
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# User and Group ID (Prevents permission issues)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Main user ID
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PUID=1000
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Main group ID:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PGID=1001
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# File permission mask
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;UMASK=0007
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Timezone (Ensures correct scheduling and logs)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;TZ=America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Define Ports (Ports for each container are defined here)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;RADARR_PORT=7878
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SONARR_PORT=8989
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SABNZBD_PORT=8080
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PROWLARR_PORT=9696
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BAZARR_PORT=6767
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;# Data Directories (Keeps storage paths centralized)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CONFIG_PATH=/docker
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;DOWNLOADS_PATH=/downloads
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;MEDIA_PATH=/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;HALLMARK_PATH=/media/Storage/Hallmark
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the &lt;code&gt;PUID&lt;/code&gt; and &lt;code&gt;PGID&lt;/code&gt; with the values you captured from &lt;code&gt;id myuser&lt;/code&gt;, set &lt;code&gt;TZ&lt;/code&gt; to your timezone, and point the paths at your real download drive and media pool.&lt;/p&gt;
&lt;p&gt;Now create the external network, since the Compose file expects it to already exist:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker network create media_network
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# PROWLARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prowlarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/prowlarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${PROWLARR_PORT}:9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/prowlarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:9696/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# SONARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sonarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sonarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SONARR_PORT}:8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sonarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Shows:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:8989/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# RADARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/radarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${RADARR_PORT}:7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${HALLMARK_PATH}:/hallmark&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:7878/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# BAZARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bazarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/bazarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;bazarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${BAZARR_PORT}:6767&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/bazarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Shows:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:6767/ || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# SABNZBD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sabnzbd:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SABNZBD_PORT}:8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# NETWORK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few things worth pointing out in that file.&lt;/p&gt;
&lt;p&gt;Every service joins &lt;code&gt;media_network&lt;/code&gt;, the user-defined bridge, so they resolve each other by container name like &lt;code&gt;http://radarr:7878&lt;/code&gt;. Each service also gets a healthcheck, so &lt;code&gt;docker ps&lt;/code&gt; tells you at a glance whether an app is genuinely up and answering rather than merely running.&lt;/p&gt;
&lt;p&gt;Notice that Prowlarr mounts only its &lt;code&gt;/config&lt;/code&gt; folder. It manages indexers and never touches your media or downloads, so there&amp;rsquo;s no reason to give it those mounts. Sonarr and Radarr each mount their library folder plus the shared &lt;code&gt;/downloads&lt;/code&gt; path, and Radarr gets the extra &lt;code&gt;/hallmark&lt;/code&gt; root on top of &lt;code&gt;/movies&lt;/code&gt;. Bazarr mounts the two media folders it writes subtitles into and nothing else.&lt;/p&gt;
&lt;p&gt;The key detail that keeps imports working is that SABnzbd, Sonarr, and Radarr all see the completed download at the exact same container path, &lt;code&gt;/downloads&lt;/code&gt;. Because that path is identical across all three, Sonarr and Radarr can find what SABnzbd finished without any remote path mapping. The media folders differ between containers (&lt;code&gt;/tv&lt;/code&gt;, &lt;code&gt;/movies&lt;/code&gt;, &lt;code&gt;/hallmark&lt;/code&gt;), but those are import destinations, so that&amp;rsquo;s fine.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Want this Compose file with your own paths, IDs, and service selection already filled in? The arr stack generator produces the &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, and launch commands from the exact template above.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/arr-stack-compose-generator/"
class="backlink-button"
target="_self"
&gt;
Generate your files
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Validate before launch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that prints your merged config without errors, you&amp;rsquo;re ready.&lt;/p&gt;
&lt;h2 id="step-4-bring-up-the-stack"&gt;Step 4: Bring Up the Stack
&lt;/h2&gt;&lt;p&gt;From &lt;code&gt;/docker&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check health:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All containers should read &lt;code&gt;Up&lt;/code&gt;, and after a minute the healthchecks should report &lt;code&gt;healthy&lt;/code&gt;. Now reach the web UIs at your VM&amp;rsquo;s IP:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prowlarr: &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:9696&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Radarr: &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:7878&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sonarr: &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:8989&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bazarr: &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:6767&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SABnzbd: &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:8080&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Do this early: open SABnzbd, run through its first-launch wizard, and set a username and password under Config, General before you expose the port anywhere beyond your LAN. While you&amp;rsquo;re there, lock down the host whitelist so only your VM and the *arr containers can reach it.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-5-configure-sabnzbd"&gt;Step 5: Configure SABnzbd
&lt;/h2&gt;&lt;p&gt;SABnzbd is the download client, so it needs your Usenet provider and a folder layout that matches the paths the containers expect.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open SABnzbd at &lt;code&gt;http://&amp;lt;vm-ip&amp;gt;:8080&lt;/code&gt; and run the startup wizard. Add your Usenet provider&amp;rsquo;s server details (host, port, SSL, username, password, and connection count) under Config, Servers.&lt;/li&gt;
&lt;li&gt;Under Config, Folders, set the temporary download folder to &lt;code&gt;/downloads/incomplete&lt;/code&gt; and the completed download folder to &lt;code&gt;/downloads/complete&lt;/code&gt;. Both of these resolve to your NVMe drive inside the container.&lt;/li&gt;
&lt;li&gt;Under Config, Categories, create two categories so Sonarr and Radarr can route their grabs:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tv&lt;/code&gt; with folder &lt;code&gt;/downloads/complete/tv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;movies&lt;/code&gt; with folder &lt;code&gt;/downloads/complete/movies&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pointing the incomplete and complete folders at the NVMe is what keeps repair and unpack fast. The completed files sit there only until Radarr or Sonarr import them onto the pool.&lt;/p&gt;
&lt;p&gt;Test: send a test NZB to the &lt;code&gt;movies&lt;/code&gt; category and confirm the finished files land in &lt;code&gt;/downloads/complete/movies&lt;/code&gt; on the host. Because SABnzbd sees &lt;code&gt;/downloads&lt;/code&gt; at the identical path the *arr apps see, no remote path mapping is needed later.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries managed by Radarr and Sonarr, where capacity per dollar matters more than raw speed.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-6-wire-prowlarr-as-the-central-indexer-hub"&gt;Step 6: Wire Prowlarr as the Central Indexer Hub
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the payoff of the shared network. Prowlarr is the Servarr indexer manager, and its job is to hold all your Usenet indexers in one place and sync them into Radarr and Sonarr automatically. If you want the deeper, single-app walkthrough for this piece, see &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-prowlarr-in-docker/" &gt;How to Install Prowlarr in Docker&lt;/a&gt;&lt;/strong&gt;. You add an indexer once in Prowlarr, and it appears in both apps.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In Prowlarr, go to Settings, Indexers, and add your Usenet indexers. These are API-based, so each one needs its API key and host. Test each one.&lt;/li&gt;
&lt;li&gt;Go to Settings, Apps, and add Radarr:
&lt;ul&gt;
&lt;li&gt;Prowlarr Server: &lt;code&gt;http://prowlarr:9696&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Radarr Server: &lt;code&gt;http://radarr:7878&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;API key: from Radarr, Settings, General&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Repeat for Sonarr with &lt;code&gt;http://sonarr:8989&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save. Prowlarr&amp;rsquo;s Sync App integration pushes every indexer into Radarr and Sonarr.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice the container names in those URLs. That only works because every app shares &lt;code&gt;media_network&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One nice thing about a Usenet-only setup: you can skip the whole FlareSolverr thing that torrent guides walk you through. FlareSolverr exists to solve Cloudflare anti-bot challenges on public torrent trackers. Usenet indexers talk over a clean API, so there&amp;rsquo;s nothing to solve and one less container to run.&lt;/p&gt;
&lt;p&gt;Test: open Radarr, Settings, Indexers, and confirm the Prowlarr indexers appear there without you adding them manually. Run an interactive search for a known movie and verify results come back.&lt;/p&gt;
&lt;h2 id="step-7-configure-radarr-and-sonarr"&gt;Step 7: Configure Radarr and Sonarr
&lt;/h2&gt;&lt;p&gt;Now connect Radarr and Sonarr to SABnzbd and set the root folders. If you only need to stand up one of these on its own, the per-app guides go deeper: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-radarr-in-docker/" &gt;install Radarr on its own&lt;/a&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;install Sonarr on its own&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="root-folders"&gt;Root folders
&lt;/h3&gt;&lt;p&gt;Set Root Folders to the media paths the containers see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Radarr: &lt;code&gt;/movies&lt;/code&gt;, and add &lt;code&gt;/hallmark&lt;/code&gt; as a second root folder if you keep that library&lt;/li&gt;
&lt;li&gt;Sonarr: &lt;code&gt;/tv&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="download-client"&gt;Download client
&lt;/h3&gt;&lt;p&gt;In Radarr, go to Settings, Download Clients, add SABnzbd:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host: &lt;code&gt;sabnzbd&lt;/code&gt; (the container name; an IP will not resolve here)&lt;/li&gt;
&lt;li&gt;Port: &lt;code&gt;8080&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;API key: from SABnzbd, Config, General&lt;/li&gt;
&lt;li&gt;Category: &lt;code&gt;movies&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repeat in Sonarr with category &lt;code&gt;tv&lt;/code&gt;. Because both apps and SABnzbd see &lt;code&gt;/downloads&lt;/code&gt; at the same path, leave remote path mappings empty. That&amp;rsquo;s what eliminates the &amp;ldquo;path does not exist&amp;rdquo; import failures.&lt;/p&gt;
&lt;p&gt;When Radarr or Sonarr import a finished download, it copies the file from &lt;code&gt;/downloads&lt;/code&gt; on the NVMe to its root folder on the pool, then tidies up the download. That copy is expected and correct for Usenet, as covered back in Step 1.&lt;/p&gt;
&lt;p&gt;Test: grab a single movie or episode, let it download and import, and confirm a finished file appears under &lt;code&gt;/media/Storage/Movies&lt;/code&gt; (or &lt;code&gt;Shows&lt;/code&gt;) on the host. A clean import with the file landing on the pool means the whole chain is wired correctly.&lt;/p&gt;
&lt;h2 id="step-8-configure-bazarr-for-subtitles"&gt;Step 8: Configure Bazarr for Subtitles
&lt;/h2&gt;&lt;p&gt;Bazarr connects to both Radarr and Sonarr, watches their libraries, and fetches subtitles automatically.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Bazarr, go to Settings, Sonarr:
&lt;ul&gt;
&lt;li&gt;Address: &lt;code&gt;sonarr&lt;/code&gt;, Port &lt;code&gt;8989&lt;/code&gt;, API key from Sonarr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Settings, Radarr:
&lt;ul&gt;
&lt;li&gt;Address: &lt;code&gt;radarr&lt;/code&gt;, Port &lt;code&gt;7878&lt;/code&gt;, API key from Radarr&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Because Bazarr mounts the same &lt;code&gt;/movies&lt;/code&gt; and &lt;code&gt;/tv&lt;/code&gt; folders, its library paths line up with what Radarr and Sonarr report. No path mapping needed.&lt;/li&gt;
&lt;li&gt;Under Settings, Languages, set your subtitle languages. Then under Settings, Providers, add a provider. OpenSubtitles.com is the usual starting point: create a free account on their site, then enter those credentials in the Bazarr provider config. If you want a second source, Podnapisi is a solid no-account-needed backup.&lt;/li&gt;
&lt;li&gt;Trigger a scan. Bazarr should list every title Radarr and Sonarr know about and start pulling subtitles.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Checkpoint: open a movie folder under &lt;code&gt;/media/Storage/Movies&lt;/code&gt; and confirm &lt;code&gt;.srt&lt;/code&gt; files show up after Bazarr runs.&lt;/p&gt;
&lt;h2 id="troubleshooting-the-common-failures"&gt;Troubleshooting the Common Failures
&lt;/h2&gt;&lt;p&gt;These are the failures I hit, in roughly the order they bit me. The FAQ at the end gives quick one-line answers. This section is the diagnostic walkthrough for when something is actually broken.&lt;/p&gt;
&lt;h3 id="import-failed-path-does-not-exist"&gt;&amp;ldquo;Import failed, path does not exist&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;This almost always means the path the download client reports does not match what Radarr or Sonarr can see. With this layout it should not happen, because SABnzbd and both *arr apps all mount the downloads drive at the identical &lt;code&gt;/downloads&lt;/code&gt; path. If it does happen, check that you didn&amp;rsquo;t accidentally rename a volume target in Compose (for example mounting &lt;code&gt;${DOWNLOADS_PATH}:/data&lt;/code&gt; in SABnzbd but leaving the *arr apps on &lt;code&gt;/downloads&lt;/code&gt;). Keep the download path identical across all three and delete any remote path mappings.&lt;/p&gt;
&lt;h3 id="radarr-or-sonarr-cant-write-to-the-pool"&gt;Radarr or Sonarr can&amp;rsquo;t write to the pool
&lt;/h3&gt;&lt;p&gt;If imports fail with permission errors on the media side, the apps can&amp;rsquo;t write to &lt;code&gt;/media/Storage&lt;/code&gt;. Confirm the &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; match a user that has write access to the mergerfs pool. This is the split-mount gotcha: downloads on the NVMe might be perfectly writable while the pool is not, because the pool&amp;rsquo;s permissions are governed at the NAS layer. Fix write access to the pool and imports start landing.&lt;/p&gt;
&lt;h3 id="files-show-as-nobodynogroup-lxc-users"&gt;Files show as nobody:nogroup (LXC users)
&lt;/h3&gt;&lt;p&gt;This is the classic unprivileged-LXC trap. The container cannot map your host user, so it sees media as &lt;code&gt;nobody&lt;/code&gt;, and imports fail. Either convert the LXC to privileged, or configure UID/GID mapping and ACLs, which is fiddly and error-prone. This is the single best reason to use a VM.&lt;/p&gt;
&lt;h3 id="containers-cannot-reach-each-other"&gt;Containers cannot reach each other
&lt;/h3&gt;&lt;p&gt;If Prowlarr cannot talk to Radarr by name, confirm both services list &lt;code&gt;media_network&lt;/code&gt; under &lt;code&gt;networks&lt;/code&gt; in Compose, that you created the external network with &lt;code&gt;docker network create media_network&lt;/code&gt; before bringing the stack up, and that you used container names, not &lt;code&gt;localhost&lt;/code&gt; or &lt;code&gt;127.0.0.1&lt;/code&gt;, in the app URLs. From inside one container you can test with &lt;code&gt;docker exec -it prowlarr ping radarr&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="permissions-sanity-check"&gt;Permissions sanity check
&lt;/h3&gt;&lt;p&gt;When in doubt, validate the chain. Run &lt;code&gt;id myuser&lt;/code&gt; on the host, confirm it matches the &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, and test a write as that user to both the download drive and the pool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u myuser touch /downloads/complete/movies/perm-test &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;downloads OK&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u myuser touch /media/Storage/Movies/perm-test &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pool OK&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="maintenance-updates-and-backups"&gt;Maintenance: Updates and Backups
&lt;/h2&gt;&lt;p&gt;Updating the stack is two commands from &lt;code&gt;/docker&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For backups, the important data is small. Back up &lt;code&gt;/docker/*&lt;/code&gt;, which holds every app&amp;rsquo;s config and database. The media under &lt;code&gt;/media/Storage&lt;/code&gt; is large, so handle that at the storage layer with NAS or pool-level snapshots rather than copying it.&lt;/p&gt;
&lt;p&gt;To grow the stack later, add more services to the same Compose file and &lt;code&gt;media_network&lt;/code&gt;: Lidarr for music, Readarr for books, or Jellyfin pointed at the same &lt;code&gt;/media/Storage&lt;/code&gt; media folders.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why are my downloads and media on different filesystems? Isn&amp;#39;t that wrong?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not for Usenet. Different filesystems only cause problems when you need hardlinks to seed torrents from the same files you import. With Usenet there&amp;rsquo;s nothing to seed, so downloading to a fast NVMe drive and importing onto a separate media pool is the right design. The one-time copy on import is expected, and it lets the NVMe handle repair and unpack at full speed.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I run the *arr suite in a Proxmox VM, on bare metal, or in an LXC?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use a VM or a bare-metal Linux box for the smoothest experience. Nothing here is Proxmox-specific, so a dedicated machine with Docker installed directly works exactly the same. Docker in an LXC, by contrast, needs a privileged container and config edits, and unprivileged LXCs with host storage often show media as nobody, breaking imports. An LXC saves resources but costs you debugging time, which is why I don&amp;rsquo;t recommend it for this stack. A VM or bare-metal box behaves like a normal Linux box.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I connect Prowlarr to Radarr and Sonarr?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;In Prowlarr go to Settings, Apps, and add each app using its container name URL (&lt;code&gt;http://radarr:7878&lt;/code&gt;, &lt;code&gt;http://sonarr:8989&lt;/code&gt;) plus the API key from that app&amp;rsquo;s Settings, General page. Prowlarr&amp;rsquo;s Sync App feature then pushes all indexers into both automatically, so you configure indexers only once.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I still need Jackett with Prowlarr?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Prowlarr is the modern Servarr indexer manager and fully replaces Jackett for Radarr, Sonarr, and the rest of the suite. It manages your indexers and syncs them directly into the apps. Older guides used Jackett, but new Prowlarr-centric stacks retire it entirely.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why does my import fail with a path error?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The download path the client reports doesn&amp;rsquo;t match what Radarr or Sonarr can see. In this setup, SABnzbd and both *arr apps all mount the download drive at &lt;code&gt;/downloads&lt;/code&gt;, so the paths line up and remote path mapping stays empty. If you renamed that mount target in one container, fix it back to &lt;code&gt;/downloads&lt;/code&gt; everywhere.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;The shortcut I learned the hard way: treat the *arr apps as one system from the start. A single Docker Compose file, one shared &lt;code&gt;media_network&lt;/code&gt; bridge, a fast NVMe download drive, and a mergerfs media pool, with matching &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; across every container, turns a fragile pile of containers into automation that genuinely works. SABnzbd does the heavy lifting on fast storage, Prowlarr feeds both apps your Usenet indexers, and Bazarr fills in subtitles without a second thought. That&amp;rsquo;s the whole arr stack docker compose payoff.&lt;/p&gt;
&lt;p&gt;From here, bring up the stack, run a test grab end to end, then layer on a media server like Jellyfin against the same &lt;code&gt;/media/Storage&lt;/code&gt;. If you came in from one of our per-app install guides for &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-radarr-in-docker/" &gt;Radarr&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;Sonarr&lt;/a&gt;&lt;/strong&gt;, or &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-prowlarr-in-docker/" &gt;Prowlarr&lt;/a&gt;&lt;/strong&gt;, this is the stack they were always meant to plug into.&lt;/p&gt;</description></item><item><title>Automated MKV Cleanup With mkvmerge, SABnzbd &amp; Cron</title><link>https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/</link><pubDate>Wed, 10 Jun 2026 06:19:48 -0600</pubDate><guid>https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/featured_hu_b9b5f5def3dad848.webp" alt="Featured image of post Automated MKV Cleanup With mkvmerge, SABnzbd &amp; Cron" /&gt;&lt;p&gt;Every media library eventually runs into the same problem. You open a movie in Jellyfin, it starts playing in a French dub you didn&amp;rsquo;t ask for, and you spend twenty seconds digging through the audio menu while the family gives you the side-eye. Multiply that by a library full of releases stuffed with five audio dubs, a commentary track, and a dozen subtitle tracks, and you have a real problem. My library had grown to roughly 2,200 MKV files, and each one was carrying around junk it did not need.&lt;/p&gt;
&lt;p&gt;The good news? This is automatable, and the right tool does it losslessly in seconds rather than hours. The bad news, which I learned the hard way, is that the obvious approach quietly destroys foreign films and anime. This post walks through how I built an automated MKV cleanup tool with mkvmerge, including every mistake I made before it worked. The whole thing is now open source: two self-contained Python scripts in the &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper" target="_blank" rel="noopener"
&gt;mkv-track-stripper repo on GitHub&lt;/a&gt;, MIT licensed, ready to drop into your own setup. You&amp;rsquo;ll get the script logic, the SABnzbd hook, the cron sweep, and the reasoning behind each safety decision. The mistakes are where the real lessons live.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Use mkvmerge to losslessly remux MKVs and strip unwanted audio and subtitle tracks, then set it up at two points: a Python SABnzbd post-processing hook (&lt;code&gt;mkv_strip_pp.py&lt;/code&gt;) cleans new downloads before Radarr and/or Sonarr imports them, and a Python library sweep (&lt;code&gt;mkvclean.py&lt;/code&gt;) handles the back catalog and can run as a cron job as an unattended catch-all. The mkvclean script tracks processed files in a checkpoint file so nothing gets processed twice, every remux is verified before the original is replaced, and forced subtitles always survive. Both scripts live in the &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper" target="_blank" rel="noopener"
&gt;mkv-track-stripper&lt;/a&gt; repo.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Debian&lt;/dt&gt;&lt;dd&gt;13 (Trixie)&lt;/dd&gt;&lt;dt&gt;Docker&lt;/dt&gt;&lt;dd&gt;29.5.2&lt;/dd&gt;&lt;dt&gt;Jellyfin&lt;/dt&gt;&lt;dd&gt;10.11.10&lt;/dd&gt;&lt;dt&gt;Radarr&lt;/dt&gt;&lt;dd&gt;6.11&lt;/dd&gt;&lt;dt&gt;SABnzbd&lt;/dt&gt;&lt;dd&gt;5.0.3&lt;/dd&gt;&lt;dt&gt;Mkvtoolnix&lt;/dt&gt;&lt;dd&gt;92.0 &amp;amp; 96.0&lt;/dd&gt;&lt;dt&gt;Python&lt;/dt&gt;&lt;dd&gt;3.13.5&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-06-10&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I ran this entire pipeline on Debian 13 with MKVToolNix 92.0 &amp;amp; 96.0, SABnzbd 5.0.3, and Radarr 6.11, validating the output in Jellyfin 10.11.10 before letting it loose on the full library. Everything in this post is a real lesson learned, scars included.&lt;/p&gt;
&lt;h2 id="why-mkvmerge-beats-ffmpeg-for-stripping-tracks"&gt;Why mkvmerge Beats ffmpeg for Stripping Tracks
&lt;/h2&gt;&lt;p&gt;For anything video-related, most people reach for ffmpeg or Tdarr first. I did too. They&amp;rsquo;re capable tools, but for stripping tracks out of MKVs at scale, mkvmerge from the MKVToolNix suite is the better fit. The reason comes down to what each tool is built for.&lt;/p&gt;
&lt;p&gt;mkvmerge is a lossless remuxer, not a transcoder. It copies streams as-is into a new MKV while letting you drop, reorder, or relabel tracks, with no re-encoding involved. The operation is fast and there&amp;rsquo;s zero quality loss. ffmpeg is designed around decode and encode processes. Copying a subset of tracks usually means more verbose &lt;code&gt;-map&lt;/code&gt; expressions, and some Matroska features don&amp;rsquo;t copy 1:1 without surprises.&lt;/p&gt;
&lt;p&gt;With mkvmerge you can keep or drop tracks by ID or by language with flags like &lt;code&gt;--audio-tracks&lt;/code&gt;, &lt;code&gt;--subtitle-tracks&lt;/code&gt;, and &lt;code&gt;--language&lt;/code&gt;, and you set default and forced flags per track. MKVToolNix is dedicated to Matroska. It handles chapters, tags, editions, and segment linking cleanly. ffmpeg&amp;rsquo;s Matroska muxer is good but not tuned for every edge case, and users report occasional player quirks with ffmpeg-muxed MKVs on ordered chapters and dual-subtitle anime.&lt;/p&gt;
&lt;h3 id="i-started-with-tdarr-and-it-was-the-wrong-tool"&gt;I Started With Tdarr, and It Was the Wrong Tool
&lt;/h3&gt;&lt;p&gt;Before I discovered MKVToolNix, I reached for Tdarr. It&amp;rsquo;s the tool everyone points you at: a slick web UI, a node-and-worker architecture, and a plugin library that promises hands-off library maintenance. I spent an evening standing up the server, attaching a node, and wiring a flow to drop the tracks I didn&amp;rsquo;t want.&lt;/p&gt;
&lt;p&gt;It was the wrong tool for two reasons. First, Tdarr is built around transcoding, and its flows kept nudging me toward re-encoding the video to &amp;ldquo;process&amp;rdquo; a file. I didn&amp;rsquo;t want a new encode. I wanted the exact same video and one or two audio tracks copied into a fresh container, untouched. Running a lossy, hours-long transcode only to delete a subtitle track is the opposite of what this job needs.&lt;/p&gt;
&lt;p&gt;Second, the whole tool was wildly oversized for the task. A server, a worker node, a database, and a huge list of plugins is a lot of moving parts to own and debug when the actual operation is &amp;ldquo;remove these track IDs.&amp;rdquo; Tdarr is genuinely good at what it&amp;rsquo;s &lt;em&gt;for&lt;/em&gt; - bulk transcoding and health-checking a library on dedicated hardware - but for lossless track stripping it buried a one-line &lt;code&gt;mkvmerge&lt;/code&gt; call under a stack of infrastructure I&amp;rsquo;d have to maintain forever. I tore it down and went back to a script.&lt;/p&gt;
&lt;p&gt;To be clear, none of this is a knock on Tdarr for the job it&amp;rsquo;s built for. If you want to &lt;strong&gt;re-encode&lt;/strong&gt; your library, say converting H.264 to H.265 to reclaim storage, Tdarr&amp;rsquo;s node-and-worker setup is the right tool. I wrote a full walkthrough for that:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/featured_hu_981ba1993ed9a22.avif"&gt;&lt;img src="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/featured_hu_c9fc9ba856f545af.webp" width="100" height="100" alt="Tdarr Setup Guide: Install, Configure &amp;amp; Transcode" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Tdarr Setup Guide: Install, Configure &amp;amp; Transcode&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/"
class="backlink-button"
target="_self"
&gt;
Read
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0C4GWPYMB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/beelink_hu_5974274794f3200a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/beelink_hu_dd7f73b5fd08d717.webp" width="600" height="481" alt="Beelink SER5 (Ryzen 5 5500U)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Beelink SER5 (Ryzen 5 5500U).&lt;/strong&gt;
A palm-sized mini PC with a 6-core/12-thread CPU that feels snappy for everyday work and homelab duties, handling Docker stacks, light VMs, and Plex without guzzling power. With NVMe plus a 2.5″ bay, Wi-Fi 6, and multi-display output, it&amp;rsquo;s a quiet, tidy upgrade for desk or media setups.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4fB1X31" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="inspect-before-you-touch-anything"&gt;Inspect Before You Touch Anything
&lt;/h3&gt;&lt;p&gt;Before automating, you need to see what&amp;rsquo;s actually inside a file. Two commands do everything:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkvmerge -i movie.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This lists tracks with their IDs, types, languages, and names in human-readable form. For scripting, you want the JSON version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkvmerge -J movie.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a machine-readable description you can parse safely. The fields that matter are &lt;code&gt;tracks[].id&lt;/code&gt;, &lt;code&gt;tracks[].type&lt;/code&gt;, &lt;code&gt;tracks[].properties.language&lt;/code&gt;, and &lt;code&gt;tracks[].properties.track_name&lt;/code&gt;. When a release is genuinely strange (ordered chapters, segment linking), &lt;code&gt;mkvinfo movie.mkv&lt;/code&gt; gives even deeper details for debugging.&lt;/p&gt;
&lt;p&gt;One thing early on that I had to learn: &lt;code&gt;mkvmerge -J&lt;/code&gt; exits with code &lt;code&gt;1&lt;/code&gt; when it has warnings about a file, but it still emits perfectly valid JSON. An early version treated that as a probe failure and skipped the file. Only exit codes of 2 or higher mean the probe actually failed.&lt;/p&gt;
&lt;h2 id="the-first-script-and-the-trap-hiding-in-it"&gt;The First Script, and the Trap Hiding in It
&lt;/h2&gt;&lt;p&gt;My first script was embarrassingly simple: keep only English audio and English subtitles, drop everything else. It looked completely reasonable. It ran fine against a folder of Hollywood blockbusters. Then I pointed it at my anime folder.&lt;/p&gt;
&lt;p&gt;Every single file came out silent.&lt;/p&gt;
&lt;p&gt;The script had stripped the only audio track in each file because the language tag said &lt;code&gt;jpn&lt;/code&gt;, not &lt;code&gt;eng&lt;/code&gt;. There was no error and no warning. Dead silence when I started movie night while everyone stared at me. That moment is when I stopped writing naive language filters and started reasoning from what tracks are actually present, rather than what I assumed should be there.&lt;/p&gt;
&lt;p&gt;The fix is conditional logic, and it&amp;rsquo;s conservative on purpose. Breaking one film is worse than failing to clean five.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a file has a single audio track, keep it no matter the language. A movie with one audio stream is never a candidate for audio removal.&lt;/li&gt;
&lt;li&gt;If a file has multiple audio tracks but none match your preferred languages, keep them all and log a notice rather than risk going silent.&lt;/li&gt;
&lt;li&gt;Never drop all subtitle tracks when the audio is in a language you don&amp;rsquo;t understand.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s no reliable original-language flag to lean on. Real-world files have audio mistagged as &lt;code&gt;und&lt;/code&gt;, fansubs with multiple &lt;code&gt;eng&lt;/code&gt; subtitle tracks (full subs versus signs-and-songs), and scene releases that label things inconsistently.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;und&lt;/code&gt; (undetermined) tag deserves its own rule, because it&amp;rsquo;s a coin flip: it might be the English track a sloppy release group forgot to label, or it might be a dub you&amp;rsquo;ll never play. The selection logic in both scripts treats &lt;code&gt;und&lt;/code&gt; as a preferred language &lt;em&gt;only until a real English track shows up&lt;/em&gt;. If the file has properly tagged English audio, &lt;code&gt;und&lt;/code&gt; tracks are no longer given the benefit of the doubt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;has_eng_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;language&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;eng&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;target_audio_langs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_langs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_eng_audio&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;und&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target_audio_langs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target_audio_langs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;und&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;Lessons learned:&lt;/strong&gt; Filtering blindly by language is the single most dangerous thing you can do to a media library. A silent movie is a broken movie. Always handle the single-track, no-preferred-match, and &lt;code&gt;und&lt;/code&gt; cases before you let any removal logic run.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="safety-as-a-first-principle"&gt;Safety as a First Principle
&lt;/h2&gt;&lt;p&gt;Once the language logic was sane, the next set of mistakes were about how I handled the files themselves. Three rules emerged, and all of them are non-negotiable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write a new file, verify it, then and only then replace the source.&lt;/strong&gt; Both scripts remux to a hidden temp file (via &lt;code&gt;tempfile.mkstemp&lt;/code&gt;) in the same directory as the original, then call &lt;code&gt;os.replace&lt;/code&gt; to swap it atomically onto the original path. Same-directory output is deliberate twice over: a rename within one filesystem is atomic, so Radarr, Sonarr or Jellyfin never catches a half-written file mid-swap, and it sidesteps cross-device link errors when your storage is a MergerFS or ZFS pool. Before any of that, a pre-flight check confirms the directory has at least 105% of the original&amp;rsquo;s size free, so a full disk can&amp;rsquo;t produce a truncated output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verify means actually verify.&lt;/strong&gt; A non-empty output file and a happy exit code aren&amp;rsquo;t proof of a good remux. Before the swap, &lt;code&gt;verify_remux()&lt;/code&gt; re-probes the temp file with &lt;code&gt;mkvmerge -J&lt;/code&gt; and confirms it still contains a video track and &lt;em&gt;exactly&lt;/em&gt; the audio and subtitle track counts that were requested. If the re-probe fails or the numbers don&amp;rsquo;t add up, the original stays put and the error is logged. This is the guard against a truncated-but-nonzero output silently overwriting a perfectly good source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read exit codes correctly.&lt;/strong&gt; This one trips people up (including me). mkvmerge does not use the standard &amp;ldquo;0 good, anything else bad&amp;rdquo; convention. It returns &lt;code&gt;0&lt;/code&gt; on full success, &lt;code&gt;1&lt;/code&gt; on success with warnings, and &lt;code&gt;2&lt;/code&gt; for a genuine error. A warning often means a track had a minor inconsistency that mkvmerge handled fine, so the scripts accept both:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getsize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only code 2, or a zero-byte output file, or a failed verification re-probe, means stop and preserve the original intact.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s one more thing the swap has to get right that I didn&amp;rsquo;t anticipate: the cleaned file is a &lt;em&gt;new&lt;/em&gt; file, so it arrives owned by whoever ran the script, with fresh permissions and timestamps. On a library shared between Radarr, Sonarr, Jellyfin, and an NFS export, that&amp;rsquo;s a quiet way to break things days later. After a successful remux, &lt;code&gt;preserve_metadata()&lt;/code&gt; copies the original&amp;rsquo;s ownership, mode, timestamps, extended attributes, and POSIX ACLs onto the cleaned file before the swap. If ownership can&amp;rsquo;t be restored (you&amp;rsquo;re not running as root or as a user who doesn&amp;rsquo;t own the files), it logs a warning and keeps going rather than dying - the cleanup still worked, the file has a new owner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The subtitle judgment call.&lt;/strong&gt; Subtitles get a more aggressive policy than audio, and I want to be honest that this is a choice rather than an obvious truth. A missing subtitle doesn&amp;rsquo;t break playback the way missing audio does - worst case, you re-add a sub later. So: keep your preferred languages, drop the rest, and drop SDH/hearing-impaired tracks along the way. But there&amp;rsquo;s one exception that&amp;rsquo;s absolute. Forced subtitles - the ones that translate the single line of Elvish in an otherwise English film - are always kept, no matter what language they&amp;rsquo;re tagged as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;is_forced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;forced_track&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;forced&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;track_name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;is_sdh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;flag_hearing_impaired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sdh&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;track_name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_forced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;keep_subs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sub_langs&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_sdh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;keep_subs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note the fallback to a &lt;code&gt;&amp;quot;forced&amp;quot;&lt;/code&gt; substring in the track name. An earlier version trusted the Matroska flag alone, and plenty of real-world releases name the track &amp;ldquo;Forced&amp;rdquo; without ever setting the flag. If you rely on SDH subtitles, add them back by removing the &lt;code&gt;is_sdh&lt;/code&gt; check or adjusting the config to your needs.&lt;/p&gt;
&lt;h2 id="the-bash-era-and-why-it-didnt-survive"&gt;The Bash Era, and Why It Didn&amp;rsquo;t Survive
&lt;/h2&gt;&lt;p&gt;The first working version of all this was a pair of Bash scripts gluing &lt;code&gt;mkvmerge -J&lt;/code&gt; to &lt;code&gt;jq&lt;/code&gt;, and they taught me a lot of lessons that anyone scripting against a real library will hit.&lt;/p&gt;
&lt;p&gt;Filenames with spaces, apostrophes, and brackets will quickly break a naive loop. A movie called &lt;code&gt;Amelie (2001) [1080p].mkv&lt;/code&gt; breaks &lt;code&gt;for f in $(find ...)&lt;/code&gt; instantly, because the shell splits on whitespace. The fix is NUL-safe handling: &lt;code&gt;find ... -print0&lt;/code&gt; piped into &lt;code&gt;IFS= read -r -d ''&lt;/code&gt;. Then the progress counter I added always reported zero at the end, because piping &lt;code&gt;find&lt;/code&gt; into a &lt;code&gt;while&lt;/code&gt; loop runs the loop in a subshell where incremented variables die on exit - the fix is process substitution, &lt;code&gt;done &amp;lt; &amp;lt;(find ...)&lt;/code&gt;. And to make re-runs skip finished files, the Bash version renamed every cleaned file to &lt;code&gt;movie.cleaned.mkv&lt;/code&gt; and excluded that pattern from the next &lt;code&gt;find&lt;/code&gt;. The name was the marker.&lt;/p&gt;
&lt;p&gt;It all worked. I still retired the whole thing, for three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The marker rename was the wrong kind of clever.&lt;/strong&gt; Renaming every file in the library means renaming it out from under Radarr, Sonarr and Jellyfin, which tracks files by path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The logic outgrew Bash.&lt;/strong&gt; Once I wanted forced-subtitle detection, junk-track flags, metadata edits, and output verification, the &lt;code&gt;jq&lt;/code&gt; one-liners turned into the hardest-to-read part of the system. The same selection logic in Python is named functions you can reason about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two scripts, one brain.&lt;/strong&gt; The cron sweep and the bulk-pass script were 90% copy-paste of each other, drifting apart with every fix. The Python rewrite collapsed them into one script, &lt;code&gt;mkvclean.py&lt;/code&gt;, that handles both jobs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The shell lessons still stand - NUL-safe loops and subshell scoping will bite you in any Bash project. But the &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper" target="_blank" rel="noopener"
&gt;shipped versions of these tools&lt;/a&gt; are Python 3.10+, end to end, with no &lt;code&gt;jq&lt;/code&gt; dependency at all.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;Lessons learned:&lt;/strong&gt; Write the quick Bash version to learn the problem, but notice the moment the logic outgrows it. For me that moment was the third nested &lt;code&gt;jq&lt;/code&gt; expression. Every safety feature that now guards my library - verification, forced-sub detection, metadata healing - would have been miserable to bolt onto the Bash scripts and was straightforward in Python.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-checkpoint-that-makes-the-whole-thing-self-repeating"&gt;The Checkpoint That Makes the Whole Thing Self-repeating
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s something worth calling out: mkvmerge doesn&amp;rsquo;t skip files it already cleaned. The tool has no memory whatsoever. It will happily re-remux a file you cleaned yesterday, every single run. Your script has to do all the skipping.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mkvclean.py&lt;/code&gt; solves this with a checkpoint file instead of the old rename-marker. Every successfully handled file is appended to &lt;code&gt;~/.mkvclean_checkpoint&lt;/code&gt; as a JSON line recording its path, modification time, and size. On the next run, any file whose path, mtime, and size all match its checkpoint entry is skipped instantly - no probe, no remux, nothing. Filenames never change, so Radarr, Sonarr, and Jellyfin are none the wiser.&lt;/p&gt;
&lt;p&gt;The mtime-and-size part does something the rename-marker never could: it detects upgrades. When Radarr replaces a movie with a better release, the new file has a new size and mtime, the checkpoint entry no longer matches, and the sweep cleans the new file automatically on its next pass.&lt;/p&gt;
&lt;p&gt;The checkpoint is append-only during a run, so it compacts itself at startup - duplicate entries collapse to one line per file and entries for files that no longer exist under the scanned root are pruned. Combined with &lt;code&gt;fcntl&lt;/code&gt; file locking (a manual run and a cron run can physically never overlap) and an &lt;code&gt;os.scandir&lt;/code&gt;-based traversal that pulls file metadata during the walk instead of &lt;code&gt;stat&lt;/code&gt;-ing everything twice, the sweep stays fast and safe on a multi-terabyte array.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--batch&lt;/code&gt; flag is the right way to start cautiously and build confidence before committing to the whole library:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;20&lt;/span&gt; &lt;span class="c1"&gt;# first run: 20 files only&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="c1"&gt;# next run: 50 more (checkpointed files are skipped)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# 0 = no limit: finish everything remaining&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Stop at any point with Ctrl+C - the script traps the signal, logs that it was interrupted, and the checkpoint means re-running always picks up exactly where you left off. It also removes the tmp file it was building.&lt;/p&gt;
&lt;p&gt;The SABnzbd hook doesn&amp;rsquo;t need a checkpoint at all. It runs once on each download&amp;rsquo;s folder right after the download completes, and it skips the remux entirely when there&amp;rsquo;s nothing to remove. A &lt;code&gt;DRY_RUN&lt;/code&gt; constant at the top (and a &lt;code&gt;--dry-run&lt;/code&gt; flag on the sweep) lets you see exactly what would happen before anything touches real files.&lt;/p&gt;
&lt;p&gt;While you&amp;rsquo;re still learning your own language rules, run in small batches and spot-check a handful of files in Jellyfin before continuing.&lt;/p&gt;
&lt;h2 id="what-gets-stripped-beyond-languages"&gt;What Gets Stripped Beyond Languages
&lt;/h2&gt;&lt;p&gt;Language filtering was the original goal, but once the scripts were reading every track&amp;rsquo;s properties anyway, a second category of junk became impossible to ignore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Commentary and descriptive audio.&lt;/strong&gt; Director&amp;rsquo;s commentary, &amp;ldquo;descriptive video service&amp;rdquo; tracks, and audio descriptions are junk for most libraries, and they&amp;rsquo;re often tagged &lt;code&gt;eng&lt;/code&gt; - so a pure language filter keeps them. The scripts detect them through the explicit Matroska flags first (&lt;code&gt;flag_commentary&lt;/code&gt;, &lt;code&gt;flag_visual_impaired&lt;/code&gt;), which catch untitled or non-English commentary tracks, then fall back to track-name matching:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;is_junk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;flag_commentary&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;flag_visual_impaired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;track_name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;JUNK_AUDIO_NAME_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;where the name patterns are &lt;code&gt;commentary&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;director&lt;/code&gt;, and &lt;code&gt;dvs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Default-flag enforcement.&lt;/strong&gt; This fixes the single most common Jellyfin complaint - the wrong audio track playing by default. The first kept audio track becomes the sole default, and the default flag is explicitly &lt;em&gt;cleared&lt;/em&gt; on every other kept track, so a stale flag carried over from the source can&amp;rsquo;t leave two defaults fighting each other:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keep_audio&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--default-track-flag&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Subtitle default flags are cleared across the board, so nothing forces subs on by default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cosmetic metadata.&lt;/strong&gt; Release groups leave a trail: the global container title set to the release filename, tag blocks, track names like &amp;ldquo;Commentary by&amp;hellip;&amp;rdquo;. The scripts strip the global title, wipe tags, and clear junk track names by default (each pass individually toggleable). They&amp;rsquo;ll also fill in an undefined track language when the track&amp;rsquo;s &lt;em&gt;name&lt;/em&gt; gives it away - a track named &amp;ldquo;English&amp;rdquo; tagged &lt;code&gt;und&lt;/code&gt; gets relabeled &lt;code&gt;eng&lt;/code&gt;, and crucially this inference runs &lt;em&gt;before&lt;/em&gt; the language filter so the corrected tag affects what&amp;rsquo;s kept. Attachment removal (cover art, embedded fonts) exists but is off by default, because embedded fonts can matter for styled ASS/SSA subtitles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The mkvpropedit fast-path.&lt;/strong&gt; Here&amp;rsquo;s a neat optimization that fell out of all this: when a file&amp;rsquo;s tracks are already exactly what you want but its &lt;em&gt;header&lt;/em&gt; is still wrong - a stale default flag, a junk title - there&amp;rsquo;s no reason to remux gigabytes. &lt;code&gt;mkvpropedit&lt;/code&gt; (also from MKVToolNix) edits headers in place on the existing file, in milliseconds, with no temp file, no disk-space requirement, and ownership/permissions preserved for free because the file never moves. The scripts plan header-only edits whenever track selection comes back unchanged, so a second pass over a clean library is nearly instant and still fixes metadata.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5).&lt;/strong&gt;
A compact mini PC with a 12th-gen Core i5-1240P and Iris Xe that can drive up to four displays (dual Thunderbolt 4 + dual HDMI), plus 2.5GbE and Wi-Fi 6E. The H-chassis adds a 2.5″ bay alongside NVMe storage and up to 64GB RAM, making it a quiet, versatile homelab node or HTPC/office box.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="moving-cleanup-into-the-pipeline-with-sabnzbd"&gt;Moving Cleanup Into the Pipeline With SABnzbd
&lt;/h2&gt;&lt;p&gt;Sweeping the entire library on a schedule works, but the smarter move is to clean each file once, at download time, before Radarr or Sonarr ever imports it. SABnzbd makes this possible with post-processing scripts. That&amp;rsquo;s the job of &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper/blob/main/mkv_strip_pp.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;mkv_strip_pp.py&lt;/code&gt;&lt;/a&gt;: drop it into your SABnzbd scripts directory, make it executable, then assign it to your &lt;code&gt;movies&lt;/code&gt; (and/or &lt;code&gt;tv&lt;/code&gt;) category in Settings → Post-Processing in the SABnzbd UI so only those downloads trigger cleanup.&lt;/p&gt;
&lt;p&gt;SABnzbd exports job details as both positional arguments and environment variables. The hook reads the job folder from &lt;code&gt;SAB_COMPLETE_DIR&lt;/code&gt; (falling back to &lt;code&gt;argv[1]&lt;/code&gt; for manual testing on the command line) and the download status from &lt;code&gt;SAB_PP_STATUS&lt;/code&gt; (falling back to &lt;code&gt;argv[7]&lt;/code&gt;). If the download was already marked failed, the hook exits immediately without touching anything.&lt;/p&gt;
&lt;p&gt;Because the cleaned file is swapped atomically onto the original path, the filename Radarr or Sonarr expects to import never changes. They never see a seam - it imports the file it expected, already trimmed.&lt;/p&gt;
&lt;p&gt;The most important design principle here: cleanup must never block an import. If stripping a file fails, leave the original intact, log it, and still let the import proceed. A working movie that didn&amp;rsquo;t get cleaned is fine. A broken import because your cleanup script choked is not.&lt;/p&gt;
&lt;h3 id="two-different-exit-code-systems-dont-mix-them"&gt;Two Different Exit-Code Systems (Don&amp;rsquo;t Mix Them)
&lt;/h3&gt;&lt;p&gt;This trips people up because two separate exit-code conventions show up in the same context, and they mean different things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SABnzbd&amp;rsquo;s exit codes&lt;/strong&gt; tell SABnzbd what to do with the job after your post-processing script runs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;SABnzbd interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry the download&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;mkvmerge&amp;rsquo;s exit codes&lt;/strong&gt; (covered earlier) describe the remux itself: &lt;code&gt;0&lt;/code&gt; success, &lt;code&gt;1&lt;/code&gt; success with warnings, &lt;code&gt;2&lt;/code&gt; real error. Same numbers, completely different meanings. If you setup your SABnzbd hook to mkvmerge&amp;rsquo;s convention, a remux warning (mkvmerge &lt;code&gt;1&lt;/code&gt;) would mark a perfectly good download as failed in SAB. That&amp;rsquo;s exactly the kind of silent breakage you don&amp;rsquo;t want.&lt;/p&gt;
&lt;p&gt;The hook keeps these rigorously separated. Any cleanup failure - mkvmerge returning &lt;code&gt;2&lt;/code&gt;, a failed verification, an unexpected exception on one file - is handled as &amp;ldquo;log it, leave the original, move on.&amp;rdquo; The hook still exits &lt;code&gt;0&lt;/code&gt; in all those cases, because a movie that wasn&amp;rsquo;t cleaned is still importable. The only time it exits &lt;code&gt;1&lt;/code&gt; is for genuine setup problems (mkvmerge or mkvpropedit not found, job directory missing), because those mean nothing can work at all.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Found &lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt; MKV file(s).&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mkvs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mkvs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Inspecting: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AUDIO_LANGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUB_LANGS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved_mkvmerge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;resolved_mkvpropedit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DRY_RUN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLEANUP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;nothing&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; Nothing to strip, leaving file as-is.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;stripped&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; Done.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;fixed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34; Metadata fixed in place (no remux).&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# never let one bad file break the import&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Unexpected error on &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;. Original kept.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Finished. Exiting 0 so import proceeds.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even the logging follows the never-block rule. SABnzbd captures the script&amp;rsquo;s stdout into its job history, and the file log (defaulting to &lt;code&gt;/config/mkv_strip_pp.log&lt;/code&gt;, which lands on the persistent volume in Docker) is best practice - an unwritable log path produces a warning instead of a crash, because an earlier version managed to fail an entire download over a log file it couldn&amp;rsquo;t open.&lt;/p&gt;
&lt;p&gt;Configuration is a block of constants at the top of the script - preferred languages, dry-run, and toggles for each cleanup pass:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;AUDIO_LANGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;eng&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jpn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;und&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;SUB_LANGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;eng&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;und&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LOG_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/config/mkv_strip_pp.log&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DRY_RUN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;STRIP_TITLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;STRIP_TAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;INFER_LANGUAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;CLEAR_JUNK_TRACK_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;STRIP_ATTACHMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="installing-mkvmerge-inside-a-docker-container"&gt;Installing mkvmerge Inside a Docker Container
&lt;/h2&gt;&lt;p&gt;If you run SABnzbd in Docker, you hit an error the first time you try to use mkvmerge inside it: the binary isn&amp;rsquo;t there. The tempting fix is &lt;code&gt;docker exec&lt;/code&gt; into the running container and installing it by hand. Don&amp;rsquo;t do this. The moment you pull a new image, your install vanishes.&lt;/p&gt;
&lt;p&gt;The correct fix is a tiny custom Dockerfile that extends the base image, and the repo &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper/blob/main/Dockerfile" target="_blank" rel="noopener"
&gt;ships one&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;lscr.io/linuxserver/sabnzbd:latest&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Add mkvmerge (from MKVToolNix) and the optional ACL so the post-processing script can run.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# The LinuxServer.io image is Alpine-based, so use apk.&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;RUN&lt;/span&gt; apk add --no-cache mkvtoolnix acl&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;apk&lt;/code&gt; line hides my actual debugging moment. My first Dockerfile used &lt;code&gt;apt-get&lt;/code&gt;, and the build died with &lt;code&gt;apt-get: not found&lt;/code&gt;. The base image was Alpine, not Debian. Alpine uses &lt;code&gt;apk&lt;/code&gt;, not &lt;code&gt;apt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;docker-compose.yaml&lt;/code&gt;, point the service at the Dockerfile with &lt;code&gt;build:&lt;/code&gt; instead of a plain &lt;code&gt;image:&lt;/code&gt;, and mount the hook into the container&amp;rsquo;s scripts directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;. &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# builds the Dockerfile in this repo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=Etc/UTC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8080:8080&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/path/to/appdata/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./mkv_strip_pp.py:/config/scripts/mkv_strip_pp.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/path/to/downloads:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One habit changes after adding packages to a custom Dockerfile. Updating the container is now &lt;code&gt;docker compose up -d --build --pull always&lt;/code&gt; rather than a plain &lt;code&gt;docker compose pull&lt;/code&gt;, so your added packages get rebuilt on top of the fresh base.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;mkvclean.py&lt;/code&gt; runs directly on the host, so the host needs Python 3.10+ and MKVToolNix there, plus the optional &lt;code&gt;acl&lt;/code&gt; package if you want POSIX ACLs preserved on cleaned files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install -y mkvtoolnix python3 acl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2.&lt;/strong&gt;
A compact mini-workstation built around up to a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. A strong homelab node or small server with serious I/O.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-cron-sweep-or-why-you-still-need-one"&gt;The Cron Sweep, or Why You Still Need One
&lt;/h2&gt;&lt;p&gt;With per-download cleanup in place, you might think the sweep is redundant. It&amp;rsquo;s not. The SABnzbd hook only sees files SABnzbd downloads. It misses manual rips, files copied in from elsewhere, media you reorganized, and anything from a category the hook isn&amp;rsquo;t attached to. A scheduled sweep is your catch-all for eventual consistency across the whole library.&lt;/p&gt;
&lt;p&gt;The sweep is &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper/blob/main/mkvclean.py" target="_blank" rel="noopener"
&gt;&lt;code&gt;mkvclean.py&lt;/code&gt;&lt;/a&gt; again - the same checkpoint-aware script from the bulk pass, run unattended. Install it somewhere on the &lt;code&gt;PATH&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cp mkvclean.py /usr/local/bin/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod +x /usr/local/bin/mkvclean.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Locking is built in: the script takes a kernel-level &lt;code&gt;fcntl&lt;/code&gt; lock on &lt;code&gt;/tmp/mkvclean.lock&lt;/code&gt; at startup and exits quietly if another run already holds it, so a cron firing while a manual run is still chewing through the library can never cause two sweeps to step on each other. Logging is built in too - everything goes to the path given by &lt;code&gt;--log&lt;/code&gt; (default &lt;code&gt;~/mkvclean.log&lt;/code&gt;) as well as stdout - so the crontab line stays clean with no redirection:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-cron" data-lang="cron"&gt;0 4 * * * /usr/local/bin/mkvclean.py /media/Storage/Movies --batch 0 --log ~/mkvclean-cron.log
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Thanks to the checkpoint, a typical nightly run skips almost everything instantly and only touches whatever&amp;rsquo;s new or upgraded since the last pass. Safe to run as often as you like.&lt;/p&gt;
&lt;h2 id="the-one-time-bulk-pass"&gt;The One-Time Bulk Pass
&lt;/h2&gt;&lt;p&gt;The third job is the one you run once: cleaning the back catalogue that existed before any of this automation. Same script, run by hand, starting with a dry run.&lt;/p&gt;
&lt;p&gt;Always do the dry run first on a new library. It reports exactly what would be stripped from every file without modifying a single byte:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --dry-run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read the log. Confirm it&amp;rsquo;s keeping what you expect - especially on anime and foreign films. Then start small, with a batch spanning different languages and sources:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Spot-check a handful of files in Jellyfin to confirm audio and subtitle selection looks right. If everything looks good, continue in larger batches:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./mkvclean.py /media/Storage/Movies --batch &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# 0 = no limit: finish everything remaining&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The language preferences are flags rather than edits to the script - &lt;code&gt;--audio eng,jpn,und --subs eng,und&lt;/code&gt; are the defaults. If you catch a bad rule, stop with Ctrl+C, adjust the flags, and re-run; checkpointed files won&amp;rsquo;t be touched again. There&amp;rsquo;s also a &lt;code&gt;--prefer-audio-channels&lt;/code&gt; flag for the hoarder special: releases carrying both a lossless 7.1 track and an AC3 5.1 track in the same language. Set it and the sweep keeps only the best English track (exact channel-count match wins, otherwise most channels) instead of all of them.&lt;/p&gt;
&lt;p&gt;Files a bad run already processed may need re-downloading; that&amp;rsquo;s exactly why you dry-run first and verify in batches rather than letting it run unattended overnight.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation.&lt;/strong&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has more than enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without turning your closet into a jet engine or space heater.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="how-jellyfin-sees-the-result"&gt;How Jellyfin Sees the Result
&lt;/h2&gt;&lt;p&gt;A quick note on the playback side, because it&amp;rsquo;s where you confirm the whole thing worked. Jellyfin uses ffmpeg for transcoding. That behavior is unaffected by whether the file was muxed with ffmpeg or mkvmerge, as long as the MKV is spec-compliant. So cleaning with mkvmerge causes Jellyfin no trouble.&lt;/p&gt;
&lt;p&gt;The two issues Jellyfin users hit most are the wrong default audio track and missing subtitles. The default-track enforcement described earlier handles the first one directly: every cleaned file comes out with exactly one default audio track (the first kept one) and no default-flagged subtitles, so Jellyfin&amp;rsquo;s track picker starts from a sane baseline instead of whatever flags the release group left behind. And when a file needs &lt;em&gt;only&lt;/em&gt; that flag fixed, the &lt;code&gt;mkvpropedit&lt;/code&gt; fast-path patches the header in place without remuxing at all, which is instant.&lt;/p&gt;
&lt;p&gt;Missing subtitles are guarded by the forced-track rule - the subtitle that translates the alien dialogue in an otherwise-English film survives every cleanup, even when it&amp;rsquo;s mistagged.&lt;/p&gt;
&lt;h2 id="the-finished-system"&gt;The Finished System
&lt;/h2&gt;&lt;p&gt;What started as a one-off cleanup grew into two scripts covering three jobs, now public as &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper" target="_blank" rel="noopener"
&gt;mkv-track-stripper on GitHub&lt;/a&gt; under the MIT license:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Per-download cleanup&lt;/strong&gt; via the SABnzbd hook (&lt;code&gt;mkv_strip_pp.py&lt;/code&gt;, runs at download time, keeps the original filename so Radarr imports cleanly).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduled catch-all sweep&lt;/strong&gt; via cron (&lt;code&gt;mkvclean.py --batch 0&lt;/code&gt;, locked with &lt;code&gt;fcntl&lt;/code&gt;, checkpoint-aware, catches everything the download path misses).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-time bulk pass&lt;/strong&gt; over the back catalogue (the same &lt;code&gt;mkvclean.py&lt;/code&gt;, dry-run first, batched, resumable, verified in Jellyfin).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The repo carries the Dockerfile for the SABnzbd image, a changelog, and a README covering setup and troubleshooting - including the one warning everyone asks about, &lt;code&gt;Could not preserve ownership ... Operation not permitted&lt;/code&gt;, which means the script isn&amp;rsquo;t root and couldn&amp;rsquo;t chown the cleaned file back to its original owner. The cleanup itself succeeded.&lt;/p&gt;
&lt;p&gt;In practice the logs show three patterns: a file with nothing to strip and a clean header is checkpointed in milliseconds, a file needing only a flag or title fix gets an in-place &lt;code&gt;mkvpropedit&lt;/code&gt; edit, and a bloated release with 30 subtitle tracks gets trimmed down in about 30 seconds, losslessly.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will this re-encode my video and lose quality?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. mkvmerge is a remuxer, not a transcoder. It copies the video and chosen audio and subtitle streams into a new container untouched, so there&amp;rsquo;s zero quality loss and the operation finishes in seconds rather than the hours a re-encode would take.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will it ever leave a movie silent?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not if you build the safety logic in. The rules that prevent it are simple: never strip the only audio track, and when no audio matches your preferred languages, keep everything and log a notice instead of dropping anything. Undetermined (&lt;code&gt;und&lt;/code&gt;) tracks only get dropped when a properly tagged English track exists alongside them. The output is also re-probed before the original is replaced, so a broken remux can never overwrite a good file.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will it delete forced subtitles?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Forced subtitles are always kept regardless of language, detected through the Matroska &lt;code&gt;forced_track&lt;/code&gt; flag or a &amp;ldquo;forced&amp;rdquo; marker in the track name. Those are the subs that translate the one line of Elvish in an otherwise-English film, and losing them genuinely breaks the movie.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does the sweep re-process files it already cleaned?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. &lt;code&gt;mkvclean.py&lt;/code&gt; records every handled file&amp;rsquo;s path, size, and modification time in a checkpoint file and skips matching files instantly on later runs. If Radarr upgrades a movie, the new size and mtime no longer match, so the replacement gets cleaned automatically on the next sweep.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does this work with Sonarr too, or only Radarr?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. You bind the SABnzbd post-processing script to a &lt;code&gt;tv&lt;/code&gt; category in the SABnzbd UI the same way you bind it to &lt;code&gt;movies&lt;/code&gt;. The library sweep doesn&amp;rsquo;t know or care about Radarr or Sonarr at all - it walks the filesystem.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why does apk add mkvtoolnix fail in my Docker image?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;code&gt;apk&lt;/code&gt; is Alpine&amp;rsquo;s package manager. If your base image is Debian or Ubuntu, you need &lt;code&gt;apt-get install mkvtoolnix&lt;/code&gt; instead. The reverse trips people up too: &lt;code&gt;apt-get: not found&lt;/code&gt; means your base is Alpine. Always check the base image distro before picking a package manager.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I list MKV tracks and their languages from the command line?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Run &lt;code&gt;mkvmerge -i file.mkv&lt;/code&gt; for a quick human-readable list of track IDs, types, and languages. For scripting, use &lt;code&gt;mkvmerge -J file.mkv&lt;/code&gt; to get machine-readable JSON you can parse safely, reading &lt;code&gt;tracks[].properties.language&lt;/code&gt; for each track.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;A few themes carried through every layer of this build, and they&amp;rsquo;re worth restating: verify before you replace, make every operation idempotent, let failures fail safe, and choose your tools for where the project is heading rather than where it started. The single most important lesson, the one that cost me a movie night, is that you reason from the tracks a file contains, never from what you assume it should contain.&lt;/p&gt;
&lt;p&gt;A good automation is rarely the first script you write. It&amp;rsquo;s the system you arrive at after the task has taught you what it needs. Mine started as a ten-line &amp;ldquo;keep only English&amp;rdquo; filter, went through a Bash-and-&lt;code&gt;jq&lt;/code&gt; era, and ended as two Python scripts with checkpointing, verification, and safety defaults baked in - now maintained in the open at &lt;a class="link" href="https://github.com/KryptikWurm/mkv-track-stripper" target="_blank" rel="noopener"
&gt;github.com/KryptikWurm/mkv-track-stripper&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to adopt this, you don&amp;rsquo;t have to rebuild it: clone the repo, run &lt;code&gt;mkvclean.py --dry-run&lt;/code&gt; against a test folder, read the log, then layer in the SABnzbd hook and the cron sweep once you trust the selection rules. Keep the verify-before-replace defaults in place, and you can roll this out across thousands of files without ever losing a movie night.&lt;/p&gt;</description></item><item><title>Best Homelab Wi-Fi &amp; Access Points: VLAN-Aware Setup</title><link>https://diymediaserver.com/post/2026/proxmox-vlan-wifi-homelab/</link><pubDate>Thu, 14 May 2026 09:49:15 -0600</pubDate><guid>https://diymediaserver.com/post/2026/proxmox-vlan-wifi-homelab/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/proxmox-vlan-wifi-homelab/featured_hu_e4bc962e4c43794.webp" alt="Featured image of post Best Homelab Wi-Fi &amp; Access Points: VLAN-Aware Setup" /&gt;&lt;p&gt;Your homelab is humming along. Proxmox running a dozen VMs, a NAS full of media. Then you connect a cheap smart plug or a guest phone to your Wi-Fi, and suddenly you realize every untrusted device on your network can see your hypervisor management interface. That&amp;rsquo;s the moment most people recognize that their flat wireless network has become a liability.&lt;/p&gt;
&lt;p&gt;The fix is a VLAN-aware wireless setup: an access point that maps each SSID to its own VLAN, a managed switch that trunks those VLANs, and a router or firewall that enforces who can talk to whom. Get this right and your IoT junk, guest devices, and homelab services live in separate broadcast domains that can&amp;rsquo;t reach each other unless you explicitly allow it. This guide walks through hardware selection, how a Proxmox VLAN bridge fits in, and deploying the whole stack end to end.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Pick a controller-based AP (TP-Link Omada or UniFi) over mesh, pair it with a managed 802.1Q switch and an OPNsense or pfSense firewall, then map each SSID to its own VLAN so IoT, guest, and homelab traffic stay isolated by default.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Proxmox VE&lt;/dt&gt;&lt;dd&gt;9.1.9&lt;/dd&gt;&lt;dt&gt;OPNsense&lt;/dt&gt;&lt;dd&gt;26.1.7&lt;/dd&gt;&lt;dt&gt;Ap&lt;/dt&gt;&lt;dd&gt;TP-Link Omada EAP610&lt;/dd&gt;&lt;dt&gt;Switch&lt;/dt&gt;&lt;dd&gt;TP-Link TL-SG2008P (8-port managed PoE)&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-05-14&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I ran this exact build on my own rack: an OPNsense VM on Proxmox, with a TP-Link TL-SG2008P managed PoE switch, and an Omada EAP610 access point feeding five SSIDs. Everything below is what actually worked, plus the mistakes that cost me an evening.&lt;/p&gt;
&lt;h2 id="what-are-the-best-homelab-wi-fi-solutions"&gt;What Are the Best Homelab Wi-Fi Solutions?
&lt;/h2&gt;&lt;p&gt;For a homelab, the best Wi-Fi comes from a &lt;strong&gt;controller-based access point&lt;/strong&gt; that tags each SSID to its own VLAN, not a consumer router or a mesh kit. After running this on my own rack, here&amp;rsquo;s the short answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best overall homelab AP:&lt;/strong&gt; a TP-Link Omada ceiling-mount (the EAP610 I run, or a current WiFi 7 EAP). Per-SSID VLAN tagging, an optional local controller, and the best cost-to-feature ratio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best for multi-AP homes:&lt;/strong&gt; UniFi, for fast roaming and one-pane policy once you&amp;rsquo;re past two or three APs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Budget entry point:&lt;/strong&gt; the Omada EAP225. 802.11ac, PoE, VLAN-aware, and cheap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid for segmentation:&lt;/strong&gt; basic consumer APs and most mesh kits (Deco, Eero). They dump every client into one VLAN.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pair whichever AP you pick with a managed 802.1Q switch and an OPNsense or pfSense firewall, then map each SSID to its own VLAN. The rest of this guide is the full build, end to end.&lt;/p&gt;
&lt;h2 id="why-vlan-aware-wi-fi-matters-for-a-homelab"&gt;Why VLAN-Aware Wi-Fi Matters for a Homelab
&lt;/h2&gt;&lt;p&gt;A flat network treats every device the same. Your laptop, your hypervisor, a $12 Wi-Fi light bulb, and your neighbor&amp;rsquo;s phone on guest Wi-Fi all sit in one broadcast domain. Any compromised device can scan for and reach anything else.&lt;/p&gt;
&lt;p&gt;VLANs split that single network into isolated segments. Each VLAN is its own broadcast domain with its own IP range, its own DHCP scope, and its own firewall policy. The access point does the wireless half of the job: it broadcasts multiple SSIDs and tags each one with a VLAN ID (VID) before handing frames to the switch.&lt;/p&gt;
&lt;p&gt;A sensible homelab segmentation looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default (Trusted)&lt;/strong&gt;: Personal laptops, phones, admin access to Proxmox and the firewall&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IoT&lt;/strong&gt;: Smart plugs, cameras, TVs, anything with sketchy firmware and cloud dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guest&lt;/strong&gt;: Internet-only, zero LAN access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure&lt;/strong&gt;: VPN egress or sensitive services that should never touch the open internet directly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homelab&lt;/strong&gt;: Lab VMs, self-hosted services, Home Assistant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s the important caveat. VLANs are an organization and policy tool, not a security guarantee on their own. They isolate broadcast domains, but a sloppy firewall rule can still expose traffic across segments. VLANs plus a default-deny firewall is the combination that protects you.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0781YXFBT"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/tp-link-eap225_hu_da99831971d7e824.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/tp-link-eap225_hu_bb15f439bac0ed47.webp" width="600" height="574" alt="TP-Link EAP225 Omada AC1350 Ceiling-Mount AP" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A ceiling-mount, PoE-powered, dual-band 802.11ac access point with a Gigabit uplink and Omada controller support. It handles 802.1Q VLANs for clean SSID-to-network mapping (Main/IoT/Guest) and delivers solid, reliable Wi-Fi for the price. The budget entry point into the Omada ecosystem.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47pN6Ey" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="choosing-your-access-point-omada-vs-unifi-vs-basic"&gt;Choosing Your Access Point: Omada vs UniFi vs Basic
&lt;/h2&gt;&lt;p&gt;The access point is the piece that makes or breaks wireless VLAN support. Here&amp;rsquo;s how the realistic options stack up for a homelab.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;VLAN tagging&lt;/th&gt;
&lt;th&gt;Controller needed&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TP-Link Omada&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, per-SSID on most current models&lt;/td&gt;
&lt;td&gt;Optional (local, hardware, or standalone)&lt;/td&gt;
&lt;td&gt;One or two APs, best cost-to-feature ratio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UniFi (Ubiquiti)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, trunks to UniFi switches work with no manual tagging&lt;/td&gt;
&lt;td&gt;Yes (cloud or self-hosted)&lt;/td&gt;
&lt;td&gt;Multi-AP houses, fast roaming, one-pane policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basic consumer AP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Nothing here. Every client lands in one VLAN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mesh (Deco, Eero)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None or token only&lt;/td&gt;
&lt;td&gt;App-based&lt;/td&gt;
&lt;td&gt;Simple untrusted Wi-Fi with no segmentation needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A few notes on the table. Omada APs let you enable multi-SSID, enable VLAN, and assign a VLAN ID (VID) to each SSID. That&amp;rsquo;s the whole workflow. It&amp;rsquo;s what I run, and the EAP610 has been reliable across five SSIDs. UniFi is a full ecosystem where the APs, switches, and gateways are designed to work together, at the cost of a steeper learning curve and a mandatory controller. A plain consumer AP with no VLAN support defeats the entire point of segmentation. And mesh kits are built for simplicity and automatic roaming. For a homelab where VLAN isolation is the whole point, mesh is the wrong tool.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
If you want simplicity and low cost, choose Omada. If you want scalability and advanced policy across many APs, choose UniFi. If a vendor only sells mesh, look elsewhere.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0FKRF82DX"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-dual-band-7_hu_ce304ea7fa95c4b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-dual-band-7_hu_6618450a0acf8e8e.webp" width="600" height="602" alt="TP-Link Omada WiFi 7 AP (Dual-Band, 2.5GbE)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A ceiling-mount WiFi 7 access point with a 2.5GbE PoE uplink, Omada controller support, and VLAN-aware SSIDs. Dual-band (2.4/5 GHz) with MLO-capable radios for lower latency. The mid-tier pick for homelab and home networks that want WiFi 7 throughput without the tri-band premium.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47x7SCk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-supporting-cast-switch-router-and-poe"&gt;The Supporting Cast: Switch, Router, and PoE
&lt;/h2&gt;&lt;p&gt;The AP can&amp;rsquo;t do this alone. You need two more pieces.&lt;/p&gt;
&lt;h3 id="a-managed-switch"&gt;A Managed Switch
&lt;/h3&gt;&lt;p&gt;You must have a managed switch that supports 802.1Q VLAN tagging. Unmanaged consumer switches can&amp;rsquo;t tag frames with VLAN IDs. Good options include the TP-Link Easy Smart line, UniFi switches, and Netgear ProSAFE. An 8-port managed switch runs roughly $50 to $150.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07GRG63P6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/tp-link-16port_hu_440a6797edcacded.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/tp-link-16port_hu_77586bf5c752fc13.webp" width="600" height="460" alt="TP-Link TL-SG116E 16-Port Easy Smart Switch" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A 16-port, fanless Gigabit switch with a metal chassis, web GUI, and the essentials (802.1Q VLANs, trunks, QoS, and IGMP snooping) without the price of a fully managed unit. It&amp;rsquo;s the budget workhorse for OPNsense and homelab builds that need solid VLAN segmentation across a lot of ports without spending triple digits.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JKmBR4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="a-router-or-firewall-for-inter-vlan-routing"&gt;A Router or Firewall for Inter-VLAN Routing
&lt;/h3&gt;&lt;p&gt;VLANs isolate Layer 2, but something has to route between them and enforce policy at Layer 3. That&amp;rsquo;s your router or firewall: pfSense, OPNsense, or a UniFi Dream Machine. It creates a logical interface per VLAN tag, runs a DHCP scope per interface, and applies firewall rules per network. An OPNsense VLAN setup on a small mini-PC, or as a VM on Proxmox, handles all of this cleanly.&lt;/p&gt;
&lt;p&gt;One tradeoff to understand. Consumer switches often route between VLANs in software, which can throttle inter-VLAN throughput to under 100 Mbps. High-end L3-capable switches offload that routing at line rate. For most homelabs, software routing is fine unless you do heavy cross-VLAN transfers, like frequent VM migrations or large file moves between your lab and NAS.&lt;/p&gt;
&lt;h3 id="poe-budget"&gt;PoE Budget
&lt;/h3&gt;&lt;p&gt;If your AP is PoE-powered, check the switch&amp;rsquo;s PoE budget. A typical AP draws 15 to 30W. A 60W PoE switch can power two to four APs. Add up the wattage of every PoE device (APs plus cameras plus anything else) and confirm the switch can supply it. If you only have one AP and no PoE switch, a single PoE injector works fine. For multiple devices, a PoE switch is far more practical.&lt;/p&gt;
&lt;h2 id="phase-1-plan-your-vlan-layout-before-touching-hardware"&gt;Phase 1: Plan Your VLAN Layout Before Touching Hardware
&lt;/h2&gt;&lt;p&gt;Skipping the planning step is the most common way these projects go sideways. Spend twenty minutes on paper first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Define your VLANs and assign VIDs.&lt;/strong&gt; A common scheme:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VLAN 1: Default&lt;/li&gt;
&lt;li&gt;VLAN 10: IoT&lt;/li&gt;
&lt;li&gt;VLAN 20: Guest&lt;/li&gt;
&lt;li&gt;VLAN 30: Secure&lt;/li&gt;
&lt;li&gt;VLAN 40: Homelab&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Map SSIDs to VLANs.&lt;/strong&gt; For example: &amp;ldquo;HomeNetwork&amp;rdquo; to VLAN 1, &amp;ldquo;IoT-Devices&amp;rdquo; to VLAN 10, &amp;ldquo;Guest-WiFi&amp;rdquo; to VLAN 20, &amp;ldquo;Secure-VPN&amp;rdquo; to VLAN 30.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document IP ranges per VLAN.&lt;/strong&gt; Keep it predictable: 192.168.1.0/24 for VLAN 1, 192.168.10.0/24 for VLAN 10, 192.168.20.0/24 for VLAN 20, and so on. Matching the third octet to the VID saves you headaches later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write a firewall intent checklist.&lt;/strong&gt; For each VLAN, what does it actually need to reach? Default gets everything. IoT gets internet plus maybe one homelab service. Guest gets internet only. Homelab gets internal services plus internet. Document the cross-VLAN flows you intend to allow, like Homelab reaching IoT on port 8123 for Home Assistant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plan your management VLAN if you run OPNsense as a VM on a single-NIC Proxmox host.&lt;/strong&gt; This one bites people. If your Proxmox box has only one physical NIC, OPNsense runs as a VM behind that same NIC, and a careless trunk change can lock you out of both the firewall and the hypervisor at once. Decide up front which VLAN carries Proxmox and OPNsense management, keep that VLAN reachable from a trusted access port, and never strip its tag from the trunk while you still need it. More on the Proxmox side later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prep the boring stuff.&lt;/strong&gt; Configure an NTP server on the router, identify your DNS resolvers (internal for homelab, public for guest), and write out a port map: which switch port goes to the AP, which to the router, which are access ports for cameras or the NAS. Label your cables as trunk or access before you plug anything in.&lt;/p&gt;
&lt;h2 id="phase-2-configure-the-managed-switch"&gt;Phase 2: Configure the Managed Switch
&lt;/h2&gt;&lt;p&gt;With the plan written, the switch comes first because everything else trunks through it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create the VLAN IDs.&lt;/strong&gt; Log into the switch web UI, find the 802.1Q VLAN section, and create VLANs 1, 10, 20, 30, and 40. Label each one clearly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure trunk ports.&lt;/strong&gt; Identify the uplink ports: one to the AP, one to the router or firewall. Set both as &lt;em&gt;tagged&lt;/em&gt; for all VLANs. Set the PVID to 1, or disable the native VLAN entirely if your switch allows it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure access ports.&lt;/strong&gt; Assign every remaining port to its intended VLAN as &lt;em&gt;untagged&lt;/em&gt;, with the PVID matching that VLAN. For example, ports 3 and 4 untagged on VLAN 10 (PVID 10) for IoT cameras, ports 5 and 6 untagged on VLAN 40 (PVID 40) for the NAS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save and verify.&lt;/strong&gt; Save the config, then check the VLAN membership table. Trunk ports should carry all VLANs; access ports should carry only their assigned VLAN. Plug a laptop into an access port and confirm it lands in the right IP range.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The trunk-vs-access distinction is the core concept here. A trunk port carries multiple tagged VLANs and connects switches, APs, and routers. An access port carries one untagged VLAN and connects an end device that knows nothing about VLANs.&lt;/p&gt;
&lt;h2 id="phase-3-configure-the-access-point"&gt;Phase 3: Configure the Access Point
&lt;/h2&gt;&lt;p&gt;Now the wireless half. This example uses a TP-Link Omada AP; UniFi follows the same logic through its controller.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Connect and access the AP.&lt;/strong&gt; Plug the AP into your laptop via Ethernet. A standalone Omada EAP610 defaults to DHCP, and falls back to 192.168.0.254 if no DHCP server answers. Set your laptop&amp;rsquo;s IP to that same subnet (something like 192.168.0.10), then open the AP&amp;rsquo;s web UI in a browser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable multi-SSID and VLAN.&lt;/strong&gt; Go to Wireless then Wireless Settings. Switch the operation mode to Multi-SSID and check Enable VLAN.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create SSIDs and assign VIDs.&lt;/strong&gt; SSID 1 &amp;ldquo;HomeNetwork&amp;rdquo; gets VID 1, SSID 2 &amp;ldquo;IoT-Devices&amp;rdquo; gets VID 10, SSID 3 &amp;ldquo;Guest-WiFi&amp;rdquo; gets VID 20, SSID 4 &amp;ldquo;Secure-VPN&amp;rdquo; gets VID 30. Save.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set the AP uplink as a trunk.&lt;/strong&gt; The AP&amp;rsquo;s uplink port to the switch must be tagged for all VLANs, and it must plug into a trunk port on the switch. Reboot the AP if needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test wireless association.&lt;/strong&gt; Connect a phone to &amp;ldquo;HomeNetwork&amp;rdquo; and confirm it gets a 192.168.1.x address. Connect another device to &amp;ldquo;IoT-Devices&amp;rdquo; and confirm 192.168.10.x. That proves each SSID lands in the right VLAN. Don&amp;rsquo;t test cross-VLAN isolation yet. Nothing is blocking that traffic until you configure firewall rules in Phase 4.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
The first time I set up my Omada AP, I forgot to set the AP uplink port as tagged. The AP itself was fine, but every SSID dumped clients onto the native VLAN regardless of the VID I assigned. The fix was one checkbox on the AP&amp;rsquo;s uplink config. If your SSIDs all land in the same VLAN, check the AP uplink before you touch anything else.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0FJQL2T2V"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-dual-band-7_hu_ce304ea7fa95c4b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-dual-band-7_hu_6618450a0acf8e8e.webp" width="600" height="602" alt="TP-Link Omada WiFi 7 AP (Tri-Band, EAP773)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A ceiling-mount WiFi 7 tri-band (2.4/5/6 GHz) access point with Multi-Link Operation, a multi-gig PoE uplink, and full Omada controller support including fast roaming and band steering. VLAN-aware SSIDs map cleanly onto OPNsense firewall zones. The top-tier pick for dense homes or offices where the dual-band AP starts to show congestion.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47x7vHW" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="phase-4-configure-the-router-or-firewall"&gt;Phase 4: Configure the Router or Firewall
&lt;/h2&gt;&lt;p&gt;This is where inter-VLAN routing and policy live. The example uses OPNsense; pfSense is nearly identical.&lt;/p&gt;
&lt;h3 id="create-vlan-interfaces"&gt;Create VLAN Interfaces
&lt;/h3&gt;&lt;p&gt;In the firewall UI, go to Interfaces then Assignments and create a virtual interface for each VLAN tag: em0.1, em0.10, em0.20, em0.30, em0.40. Assign each one a static IP that becomes the gateway for that VLAN, like 192.168.10.1 for VLAN 10.&lt;/p&gt;
&lt;h3 id="enable-dhcp-per-vlan"&gt;Enable DHCP per VLAN
&lt;/h3&gt;&lt;p&gt;Under Services then DHCP Server, enable DHCP on each VLAN interface and set a range, for example 192.168.10.100 to 192.168.10.254. Set DNS servers per interface: an internal resolver for the Homelab VLAN, public DNS for Guest. Scoping DNS per VLAN prevents clients from resolving through the wrong resolver.&lt;/p&gt;
&lt;h3 id="set-static-ips-for-servers-and-vms"&gt;Set Static IPs for Servers and VMs
&lt;/h3&gt;&lt;p&gt;While you&amp;rsquo;re in the DHCP Server section, handle your fixed-address devices. Homelab services like a NAS or a Home Assistant VM want a stable IP, and you have two clean ways to get one. Use a DHCP reservation on OPNsense, which maps a MAC address to a fixed IP, or set a manual static IP on the device itself, chosen from outside the DHCP range. For example, if your Homelab DHCP pool is 192.168.40.100 to 254, assign your NAS 192.168.40.50 manually. I prefer reservations for VMs because the config lives in one place on the firewall, right next to the DHCP scope from the previous step.&lt;/p&gt;
&lt;h3 id="configure-firewall-rules"&gt;Configure Firewall Rules
&lt;/h3&gt;&lt;p&gt;This is the part that actually enforces isolation. Follow these golden rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Allow established and related connections so replies flow automatically.&lt;/li&gt;
&lt;li&gt;Deny cross-segment initiation by default.&lt;/li&gt;
&lt;li&gt;Grant &amp;ldquo;LAN to anywhere&amp;rdquo; only to the trusted Default network.&lt;/li&gt;
&lt;li&gt;Add explicit allow rules for the specific services you need.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A concrete example: block VLAN 10 (IoT) from initiating any connection to VLAN 40 (Homelab), then add one narrow rule allowing VLAN 40 to reach a specific IoT device on port 8123 for Home Assistant. Block VLAN 20 (Guest) from every internal VLAN while allowing it out to the internet.&lt;/p&gt;
&lt;h3 id="test-routing-and-isolation"&gt;Test Routing and Isolation
&lt;/h3&gt;&lt;p&gt;Now that firewall rules exist, this is the test that was skipped in Phase 3. Connect a device to the Default SSID and try to ping a device on the IoT SSID: it should fail. Ping the router&amp;rsquo;s IoT interface (192.168.10.1): it should succeed, because gateway access is allowed. Confirm every VLAN has working internet.&lt;/p&gt;
&lt;h2 id="proxmox-and-opnsense-integration"&gt;Proxmox and OPNsense Integration
&lt;/h2&gt;&lt;p&gt;Running this on Proxmox adds one wrinkle. This is where the Proxmox VLAN setup ties into everything you built in the earlier phases.&lt;/p&gt;
&lt;p&gt;In Proxmox, create a VLAN-aware Linux bridge. Under Datacenter then your node then System then Network, edit the bridge (usually &lt;code&gt;vmbr0&lt;/code&gt;) and check &amp;ldquo;VLAN aware&amp;rdquo;. Once the bridge is VLAN-aware, set the VLAN Tag field on each VM&amp;rsquo;s network device to the VID you want, and the VM inherits that VLAN&amp;rsquo;s membership. A Home Assistant VM tagged with VID 40 gets an IP from the Homelab DHCP scope automatically.&lt;/p&gt;
&lt;p&gt;If you run OPNsense as a VM rather than bare metal, give it two interfaces: one for WAN (untagged, or tagged to your WAN VLAN), and one for LAN connected to the VLAN-aware bridge. OPNsense then handles all inter-VLAN routing and firewall policy for both your physical and virtual machines. The physical switch trunks to the Proxmox host, and every VLAN-aware VM downstream picks up its tag from the bridge.&lt;/p&gt;
&lt;p&gt;One more warning if you only have a single physical NIC on the Proxmox host. Both Proxmox management and the OPNsense VM share that one link, so a bad trunk change can cut you off from the hypervisor and the firewall in the same move. This is why Phase 1 told you to plan a management VLAN. Keep that VLAN tagged on the trunk, keep a trusted access port that can reach it, and make trunk changes when you have console access to the Proxmox host as a fallback.&lt;/p&gt;
&lt;h2 id="ap-placement-ceiling-vs-desk"&gt;AP Placement: Ceiling vs Desk
&lt;/h2&gt;&lt;p&gt;Where the AP physically sits affects coverage more than people expect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ceiling mount&lt;/strong&gt; gives better coverage with fewer obstacles between the radio and your devices, and it looks cleaner. It needs PoE or ceiling-run power.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Desk placement&lt;/strong&gt; is easier for testing and troubleshooting since the AP is right there, but you may get dead zones and visible cable runs.&lt;/p&gt;
&lt;p&gt;For a homelab, if you have PoE, mount it on the ceiling. If it sits on a desk or other horizontal surface, make sure a PoE injector or wall outlet is within reach. If you need more than one AP, stagger them across rooms or floors and use the same SSID and VLAN per network so clients roam between them without dropping the connection.&lt;/p&gt;
&lt;h2 id="troubleshooting-common-failures"&gt;Troubleshooting Common Failures
&lt;/h2&gt;&lt;p&gt;Most VLAN Wi-Fi problems trace back to a handful of misconfigurations. Here are the ones I&amp;rsquo;ve hit or watched others hit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Devices not getting an IP.&lt;/strong&gt; Check three things in order: DHCP is enabled on the router&amp;rsquo;s VLAN interface, the DHCP scope has free addresses, and the switch access port PVID matches the device&amp;rsquo;s VLAN.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Devices landing in the wrong VLAN.&lt;/strong&gt; Verify the AP&amp;rsquo;s SSID-to-VID mapping, then check the switch access port PVID. A PVID that doesn&amp;rsquo;t match the intended VLAN is the single most common cause: frames arriving untagged get stamped with the port&amp;rsquo;s PVID, so a wrong PVID means a wrong IP range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mismatched native VLANs.&lt;/strong&gt; If two trunk ports have different native VLANs, untagged frames silently land in the wrong segment. The fix is to tag everything and skip the native VLAN where you can.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No internet on a VLAN.&lt;/strong&gt; Confirm the router has a default route to WAN and that firewall rules permit internet egress from that VLAN.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slow inter-VLAN traffic.&lt;/strong&gt; If transfers between VLANs crawl, the switch is probably routing in software. Accept it for a homelab, route through a capable CPU host like your Proxmox box, or buy L3 hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DNS not resolving.&lt;/strong&gt; Make sure DNS servers are configured on each VLAN interface and that the firewall allows port 53. Verify the client is using the expected resolver with &lt;code&gt;nslookup&lt;/code&gt; or &lt;code&gt;dig&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You locked yourself out.&lt;/strong&gt; If you misconfigure a trunk port and lose connectivity, most switches let you revert to factory defaults or reach the CLI over a serial console. Export your switch and firewall configs at every milestone so you can roll back instead of starting over.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stale firmware.&lt;/strong&gt; Older AP and switch firmware sometimes ships VLAN bugs. Update before you rely on the setup in production.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What are the best Wi-Fi access points for a homelab?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Controller-based access points that support per-SSID VLAN tagging. For most homelabs a TP-Link Omada ceiling-mount AP (such as the EAP610 or a current WiFi 7 EAP) is the best balance of price and features; step up to UniFi once you run several APs and want fast roaming and unified policy. Avoid basic consumer APs and mesh kits like Deco or Eero, which can&amp;rsquo;t tag SSIDs to separate VLANs.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need a managed switch to use VLANs?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Unmanaged consumer switches cannot tag 802.1Q frames, so they cannot carry multiple VLANs on one link. You need a managed switch such as a TP-Link Easy Smart, UniFi switch, or Netgear ProSAFE that exposes VLAN configuration in its UI.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use a mesh Wi-Fi system like Eero or Deco with VLANs?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Usually not. Most mesh systems have no VLAN tagging or only very limited support. If VLAN isolation is your goal, use a controller-based AP like UniFi or TP-Link Omada instead. Mesh is fine only for a simple untrusted network with no segmentation needs.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What is the difference between a VLAN ID and a PVID?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The VLAN ID (VID) is the tag carried inside 802.1Q frames on a trunk. The PVID (Port VLAN ID) is the untagged VLAN assigned to a switch port: any frame arriving untagged gets stamped with that port&amp;rsquo;s PVID. On access ports, always confirm the PVID matches the device&amp;rsquo;s intended VLAN.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I get a static IP for a Proxmox VM or NAS?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Two ways. Set a DHCP reservation on your firewall that maps the device&amp;rsquo;s MAC address to a fixed IP, or configure a manual static IP on the device itself, chosen from outside the DHCP range. For a homelab, reservations keep all the config in one place on the router.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do VLANs give the same security as physically separate networks?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. VLANs isolate broadcast domains and make policy easy to apply, but a single misconfigured firewall rule can still expose traffic across segments. Treat VLANs as an organization and policy layer, and always pair them with a default-deny firewall ruleset.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;A VLAN-aware wireless setup turns a flat, fragile homelab network into something you can trust. The recipe is consistent: a controller-based AP that tags SSIDs to VLANs, a managed 802.1Q switch that trunks them, and an OPNsense or pfSense firewall that routes between VLANs and enforces a default-deny policy. Skip mesh if isolation matters, plan your VLAN layout on paper first, and verify each phase before moving to the next.&lt;/p&gt;
&lt;p&gt;Start small. One AP, one managed switch, five VLANs, and a firewall is enough to isolate your IoT junk from your hypervisor. Once that&amp;rsquo;s stable, you can add APs for roaming, tighten firewall rules per service, and integrate your Proxmox VLAN bridge so VMs land in the right segment automatically.&lt;/p&gt;
&lt;p&gt;Next steps: back up your switch and firewall configs, document your final VLAN and IP layout, and if you&amp;rsquo;re running OPNsense on Proxmox, check our other guides on hardening that firewall VM and on building out the rest of your homelab stack.&lt;/p&gt;</description></item><item><title>Build Your Own NAS Storage with MergerFS (2026 Guide)</title><link>https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/</link><pubDate>Sun, 03 May 2026 13:46:27 -0600</pubDate><guid>https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/featured_hu_19131806093df532.webp" alt="Featured image of post Build Your Own NAS Storage with MergerFS (2026 Guide)" /&gt;&lt;p&gt;If you&amp;rsquo;ve ever stared at six mismatched hard drives of different sizes and wished there was a way to smash them into one big folder without buying into the ZFS religion, MergerFS is the answer. It pools whatever junk you have lying around and presents it as a single mount point that Jellyfin, Sonarr, and Radarr treat like one giant disk. When you build your own NAS storage for media, this is the path that scales without buying 8 matched drive you don&amp;rsquo;t need. Especially now with HDD prices through the roof.&lt;/p&gt;
&lt;p&gt;This 2026 guide walks through what MergerFS does, why it beats ZFS for a movie/TV library, the creation policies that matter (mfs, lfs, epff, epmfs, pfrd), how to bolt SnapRAID on for parity, and exactly what to put in fstab so your NAS will boot cleanly even when one disk is misbehaving.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
MergerFS pools multiple drives into one mount without ZFS&amp;rsquo;s upfront costs, lets you mix sizes and brands, and pairs cleanly with SnapRAID for parity. Use &lt;code&gt;category.create=mfs&lt;/code&gt; for a balanced media pool, &lt;code&gt;epmfs&lt;/code&gt; if you organize libraries by top-level folder, and always set &lt;code&gt;minfreespace&lt;/code&gt; plus &lt;code&gt;allow_other&lt;/code&gt; before pointing Docker at it.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;OS&lt;/dt&gt;&lt;dd&gt;Debian 13 (Trixie)&lt;/dd&gt;&lt;dt&gt;Kernel&lt;/dt&gt;&lt;dd&gt;6.12.85-1&lt;/dd&gt;&lt;dt&gt;MergerFS&lt;/dt&gt;&lt;dd&gt;2.40.2-5&lt;/dd&gt;&lt;dt&gt;SnapRAID&lt;/dt&gt;&lt;dd&gt;12.4-1&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-05-03&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I set this up on an and old gamming rig I had in the closet and through in four 18TB, two 24TB, drives plus a 4TB WD Red I refused to get rid of. The same fstab line has survived two drive swaps and several kernel upgrades without me having to do anything. Everything below is what I ran on that box, with the gotchas that actually bit me called out under the &amp;ldquo;Tips and Warning&amp;rdquo; boxes.&lt;/p&gt;
&lt;h2 id="what-mergerfs-is-and-why-it-is-not-raid"&gt;What MergerFS Is (And Why It Is Not RAID)
&lt;/h2&gt;&lt;p&gt;MergerFS is a FUSE-based union filesystem written by Antonio &amp;ldquo;trapexit&amp;rdquo; Musumeci. It takes a list of source directories (usually individual drives mounted at &lt;code&gt;/mnt/pool/disk1&lt;/code&gt;, &lt;code&gt;/mnt/pool/disk2&lt;/code&gt;, etc.) and presents them as one merged directory tree at a target mount like &lt;code&gt;/mnt/storage&lt;/code&gt;. The drives keep their own filesystems (ext4, XFS, ZFS, Btrfs, whatever), keep their own data, and stay independently readable if MergerFS ever disappears. Pull a drive out, slap it into a USB dock on a different machine, and your files are right there in plain folders.&lt;/p&gt;
&lt;p&gt;That last point is why it eats traditional RAID and ZFS for lunch in a media server context.&lt;/p&gt;
&lt;h3 id="the-case-against-raid-for-a-movie-library"&gt;The case against RAID for a movie library
&lt;/h3&gt;&lt;p&gt;Traditional RAID (RAID5, RAID6, RAIDZ) treats your drives as one striped volume. The wins are real: parallel read speed, transparent fault tolerance, and a single namespace. The losses are also real, and they hit homelabs harder than they hit datacenters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Same-batch failure risk&lt;/strong&gt;: drives bought together tend to die together. A second failure during a multi-day RAID5 rebuild on 14TB+ disks is a good way to lose everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No mismatched sizes&lt;/strong&gt;: most arrays force the smallest drive&amp;rsquo;s capacity on every member. Your 14TB sits there pretending to be 4TB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expansion is painful&lt;/strong&gt;: adding a drive usually means a full reshape or a brand-new vdev.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total loss on catastrophic failure&lt;/strong&gt;: lose enough drives in the wrong combination and the entire array is gone, including everything that sat on the healthy drives.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MergerFS sidesteps all four. Each drive is its own filesystem with its own files. If a drive dies, you lose only what was on that drive, not the pool. Mismatched sizes are the design, not a workaround. Adding storage is &amp;ldquo;format new drive, add it to the wildcard, remount.&amp;rdquo; That last property is why the unraid and jbod school of media servers exists at all: pooling without striping, so growth and failure are both easy on your sanity and wallet.&lt;/p&gt;
&lt;p&gt;The tradeoff is honest. MergerFS doesn&amp;rsquo;t give you striped read speeds. It doesn&amp;rsquo;t transparently survive a drive failure on its own (you need SnapRAID or backups for that). Small random writes are slower than a native filesystem because they go through FUSE. For a media server where files are huge, written once, and read sequentially, none of those matter. For a database or a VM datastore, they matter a lot. Use the right tool.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Don&amp;rsquo;t use a MergerFS pool as a download or torrent target. These small reads and writes will cause the pool to slow to a crawl. Use fast storage like SSD or NVMe drives as the download target and then move the files to the MergerFS pool once it is downloaded. Sornarr and Radarr are great at post-processing and moving to a storage pool once the download is completed.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="prerequisites-and-drive-prep"&gt;Prerequisites and Drive Prep
&lt;/h2&gt;&lt;p&gt;Before MergerFS does anything useful, your drives need to be partitioned, formatted, and mounted individually. The pool is a view. The data lives on the source filesystems. You should be comfortable running commands as root and editing &lt;code&gt;/etc/fstab&lt;/code&gt;. If &lt;code&gt;sudo&lt;/code&gt; and &lt;code&gt;vim /etc/fstab&lt;/code&gt; make you nervous, fix that first.&lt;/p&gt;
&lt;h3 id="0-check-drive-health-before-you-touch-it"&gt;0. Check drive health before you touch it
&lt;/h3&gt;&lt;p&gt;Look. If you&amp;rsquo;re adding shucked drives, recertified drives, drives from eBay, or anything that has been sitting in a closet, run a health check first. Formatting a dying disk is a great way to lose hours and discover the problem at 80% of a SnapRAID sync.&lt;/p&gt;
&lt;p&gt;Install &lt;code&gt;smartmontools&lt;/code&gt; and run a short test on each drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install smartmontools
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smartctl -t short /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To see the progress and review the results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smartctl -l selftest /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smartctl -a /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You want &lt;strong&gt;PASSED&lt;/strong&gt; under the SMART overall-health status, low values for &lt;code&gt;Reallocated_Sector_Ct&lt;/code&gt;, &lt;code&gt;Current_Pending_Sector&lt;/code&gt;, and &lt;code&gt;Offline_Uncorrectable&lt;/code&gt;, and a power-on hours count that matches what the seller advertised. If anything looks ugly, run a long self-test (&lt;code&gt;sudo smartctl -t long /dev/sdb&lt;/code&gt;, then check back in a few hours with &lt;code&gt;sudo smartctl -l selftest /dev/sdb&lt;/code&gt;) before committing the disk to the pool.&lt;/p&gt;
&lt;h3 id="1-install-mergerfs-from-upstream-not-apt"&gt;1. Install MergerFS from upstream, not apt
&lt;/h3&gt;&lt;p&gt;The Ubuntu/Debian repository version lags the upstream release by months and is missing fixes that matter (the &lt;code&gt;noforget&lt;/code&gt; and &lt;code&gt;inodecalc&lt;/code&gt; improvements, in particular). Pull the latest .deb from the GitHub releases page &lt;a class="link" href="https://github.com/trapexit/mergerfs/releases" target="_blank" rel="noopener"
&gt;GitHub mergerfs releases&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://github.com/trapexit/mergerfs/releases/download/2.42.0/mergerfs_2.42.0.debian-trixie_amd64.deb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dpkg -i mergerfs_2.42.0.debian-trixie_amd64.deb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mergerfs --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Substitute &lt;code&gt;trixie&lt;/code&gt; for &lt;code&gt;bookworm&lt;/code&gt;/&lt;code&gt;forky&lt;/code&gt;/etc. depending on your distro. Verify the version is current (2.42.0 at the time of writing).&lt;/p&gt;
&lt;h3 id="2-partition-and-format-each-drive"&gt;2. Partition and format each drive
&lt;/h3&gt;&lt;p&gt;You can use ext4 for media drives because it&amp;rsquo;s boring, well-supported, and resizes cleanly. I use XFS, I find it offers slightly better large-file performance. Avoid ZFS on individual MergerFS branches unless you specifically want ZFS features, because the FUSE layer eats most of the benefits.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo parted /dev/sdb -- mklabel gpt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo parted /dev/sdb -- mkpart primary ext4 0% 100%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkfs.ext4 -L disk1 /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/pool/disk1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Repeat for each disk, incrementing the label and mount point. Add each one to &lt;code&gt;/etc/fstab&lt;/code&gt; by UUID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo blkid /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# UUID=&amp;#34;abc123...&amp;#34; TYPE=&amp;#34;ext4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;blkid&lt;/code&gt; returns nothing, the partition table didn&amp;rsquo;t get written. Re-run the &lt;code&gt;parted&lt;/code&gt; commands and check &lt;code&gt;lsblk&lt;/code&gt; to confirm &lt;code&gt;/dev/sdb1&lt;/code&gt; exists before formatting.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-fstab" data-lang="fstab"&gt;UUID=abc123... /mnt/pool/disk1 ext4 defaults,noatime,nofail 0 2
UUID=def456... /mnt/pool/disk2 ext4 defaults,noatime,nofail 0 2
UUID=ghi789... /mnt/pool/disk3 ext4 defaults,noatime,nofail 0 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;nofail&lt;/code&gt; is non-negotiable. Without it, a single failed disk will drop your server into emergency mode at boot. Reboot and run &lt;code&gt;df -h&lt;/code&gt; to confirm every disk shows up before going further.&lt;/p&gt;
&lt;h3 id="3-decide-on-a-layout"&gt;3. Decide on a layout
&lt;/h3&gt;&lt;p&gt;Two layouts dominate. Pick one before you copy data, because changing later is annoying.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flat pool&lt;/strong&gt;: every disk has the same top-level structure (or no structure at all) and MergerFS distributes new files across them by free space. Use &lt;code&gt;category.create=mfs&lt;/code&gt;. Simplest, works for everyone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path-aware pool&lt;/strong&gt;: every disk has the same top-level folder names (&lt;code&gt;/mnt/pool/disk1/movies/&lt;/code&gt;, &lt;code&gt;/mnt/pool/disk1/tv/&lt;/code&gt;, &lt;code&gt;/mnt/pool/disk2/movies/&lt;/code&gt;, &lt;code&gt;/mnt/pool/disk2/tv/&lt;/code&gt;) and MergerFS routes new files to the disk that already has the matching path. Use &lt;code&gt;category.create=epmfs&lt;/code&gt;. Better for &amp;ldquo;I want all of Breaking Bad on one disk so a failure only loses Breaking Bad&amp;rdquo; thinking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I run &lt;code&gt;mfs&lt;/code&gt; for my library.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CSPCFKR9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ironwolf-24tb_hu_fb22fc25aed7cd30.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ironwolf-24tb_hu_1da8908f73983a8e.webp" width="600" height="869" alt="Seagate IronWolf Pro 24TB NAS Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
The flagship IronWolf Pro for multi-bay NAS enclosures. 7200 RPM CMR with a 512 MB cache and a 550 TB/year workload rating lets you stack three or four of these in a MergerFS pool without cooking them. A 5-year warranty and 3 years of Seagate Rescue data recovery are included, because when (not if) one dies, you will want a parachute.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4cSZxv5" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A pool of identical drives isn&amp;rsquo;t the goal here. But if you&amp;rsquo;re starting fresh and want to seed a multi-bay build with one or two big disks you can grow around, this is the slot they fit into.&lt;/p&gt;
&lt;h2 id="the-policy-cheat-sheet-mfs-vs-lfs-vs-epff-vs-epmfs-vs-pfrd"&gt;The Policy Cheat Sheet (mfs vs lfs vs epff vs epmfs vs pfrd)
&lt;/h2&gt;&lt;p&gt;This is the part of MergerFS that confuses everyone. There are policies for create, search, and action operations. For media servers, you almost always only care about &lt;code&gt;category.create&lt;/code&gt;. The relevant create policies, in plain English:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mfs (most free space)&lt;/strong&gt;: write the new file to the branch with the most free space. Best general-purpose pick. Spreads writes evenly so disks fill at roughly the same rate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lfs (least free space)&lt;/strong&gt;: write to the branch with the least free space (that still has room). Fills disks one at a time. Useful if you want most of your drives spun down most of the time, at the cost of zero balancing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epff (existing path, first found)&lt;/strong&gt;: only write to a branch that already has the parent path; pick the first one found. Path-aware but order-dependent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;epmfs (existing path, most free space)&lt;/strong&gt;: only write to a branch that already has the parent path; pick the one with the most free space among those. Path-aware and balanced. The right pick for organized libraries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pfrd (proportional fill, random distribution)&lt;/strong&gt;: weighted random pick by percentage free. The default in current MergerFS releases. Statistically balances drives without the &amp;ldquo;always picks the same disk&amp;rdquo; failure mode that mfs can have when sizes are mismatched [trapexit docs].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the build-your-own-NAS-storage crowd: start with &lt;code&gt;mfs&lt;/code&gt; if your library is flat, &lt;code&gt;epmfs&lt;/code&gt; if you organize by &lt;code&gt;/movies&lt;/code&gt;, &lt;code&gt;/tv&lt;/code&gt;, &lt;code&gt;/music&lt;/code&gt; folders, and only deviate if you&amp;rsquo;ve got a specific reason. Ignore lfs unless you have actually measured the spin-up cost on your hardware.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
I spent a week debugging &amp;ldquo;why is disk1 always 90% full and disk4 always empty&amp;rdquo; before I realized I was using &lt;code&gt;epff&lt;/code&gt; with a single existing path on disk1. Switched to &lt;code&gt;mfs&lt;/code&gt; and balance restored itself within a few weeks of new downloads. If you do not need path stickiness, do not opt into it.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="fstab-configuration-that-survives-reboots"&gt;fstab Configuration That Survives Reboots
&lt;/h2&gt;&lt;p&gt;The fstab line is where most first-time MergerFS setups break. Here is the line I run on Debian 13, broken down option by option:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-fstab" data-lang="fstab"&gt;# List EVERY source disk in x-systemd.after.
/mnt/pool/disk* /mnt/storage fuse.mergerfs defaults,allow_other,inodecalc=path-hash,cache.files=off,dropcacheonclose=true,category.create=epmfs,minfreespace=50G,fsname=mergerfs,nofail,x-systemd.after=/mnt/pool/disk1,x-systemd.after=/mnt/pool/disk2,x-systemd.after=/mnt/pool/disk3 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mnt/pool/disk*&lt;/code&gt; (the source): glob expansion picks up every numbered disk mount. Add a new disk by mounting it at &lt;code&gt;/mnt/pool/disk4&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaults&lt;/code&gt;: standard mount options.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allow_other&lt;/code&gt;: lets users other than root see the pool. Required for Docker containers, Jellyfin, Sonarr, Radarr, Samba, NFS, and pretty much anything useful. Without it your apps see an empty directory.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inodecalc=path-hash&lt;/code&gt;: hashes the relative path of the entry in question. This means the inode value will always be the same for that file path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache.files=off&lt;/code&gt; + &lt;code&gt;dropcacheonclose=true&lt;/code&gt;: disables FUSE page caching and drops kernel cache when files close. Counter-intuitive but correct for media. The kernel already caches reads from the underlying filesystems, and the FUSE cache layer mostly causes stale-data bugs in apps that mmap files (Jellyfin&amp;rsquo;s metadata scanner is one).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category.create=epmfs&lt;/code&gt;: see the policy section above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minfreespace=50G&lt;/code&gt;: refuse to write to a branch with less than 50GB free. Stops MergerFS from cramming the last 200MB onto a nearly-full disk and producing ENOSPC errors mid-import.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsname=mergerfs&lt;/code&gt;: cosmetic, makes &lt;code&gt;df -h&lt;/code&gt; show &lt;code&gt;mergerfs&lt;/code&gt; instead of a random path.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nofail&lt;/code&gt;: don&amp;rsquo;t block boot if the source disks aren&amp;rsquo;t ready.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x-systemd.after=/mnt/pool/diskN&lt;/code&gt;: tells systemd to wait for those source mounts before trying the pool. Without this, on slow-spinning HDD systems, fstab tries to mount the pool before the disks are mounted, and you get a ghost empty pool. &lt;strong&gt;List every disk in the pool.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;The SnapRAID + MergerFS calculator builds this exact fstab line for your disk layout, plus the &lt;code&gt;snapraid.conf&lt;/code&gt; and parity sizing to go with it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/snapraid-calculator/"
class="backlink-button"
target="_self"
&gt;
Generate your fstab line
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;After editing &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see the combined size of all source disks. Write a test file, then check which physical disk it landed on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test&amp;#34;&lt;/span&gt; &amp;gt; /mnt/storage/hello.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /mnt/pool/disk*/hello.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the pool mounts cleanly, the next thing you&amp;rsquo;ll care about is what&amp;rsquo;s under the hood. If you&amp;rsquo;re running drives off motherboard SATA today, you&amp;rsquo;ll hit the port count ceiling fast. The fix is an HBA in IT mode.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CFRKNJXL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9300-8i_hu_d34514ef05caf068.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9300-8i_hu_86413786deef51f7.webp" width="600" height="449" alt="LSI 9300-8i IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
An 8-port 62 Gb/s SAS/SATA HBA that, when flashed to IT firmware, passes each drive straight to the OS (no RAID). Perfect for MergerFS, ZFS/TrueNAS, Unraid, and clean JBOD setups.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3R5XJU0" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;With the HBA sorted, the fstab line above is portable: the source glob doesn&amp;rsquo;t care whether the drives hang off motherboard SATA or an LSI card.&lt;/p&gt;
&lt;h2 id="performance-tuning-for-jellyfin-sonarr-and-radarr"&gt;Performance Tuning for Jellyfin, Sonarr, and Radarr
&lt;/h2&gt;&lt;p&gt;The defaults above are tuned for media. A few extra tweaks help when you&amp;rsquo;re pushing 4K remuxes around.&lt;/p&gt;
&lt;h3 id="permissions-and-ownership"&gt;Permissions and ownership
&lt;/h3&gt;&lt;p&gt;Pick a UID/GID for your media stack and use it everywhere. I use &lt;code&gt;media:media&lt;/code&gt; (UID 1000, GID 1000) on the host, mapped through every Docker container with &lt;code&gt;PUID=1000&lt;/code&gt;/&lt;code&gt;PGID=1000&lt;/code&gt;. Then chown the pool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R media:media /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod -R &lt;span class="m"&gt;775&lt;/span&gt; /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mismatched UIDs between host and containers are the number-one cause of &amp;ldquo;Sonarr can see the file but Jellyfin can&amp;rsquo;t play it.&amp;rdquo; Pick one and stick with it.&lt;/p&gt;
&lt;h3 id="optional-ssd-cache-branch"&gt;Optional SSD cache branch
&lt;/h3&gt;&lt;p&gt;MergerFS doesn&amp;rsquo;t have a true write-back cache, but you can fake one with a &amp;ldquo;tiered cache&amp;rdquo; pattern. Mount an SSD at &lt;code&gt;/mnt/cache&lt;/code&gt;, build a second pool that prefers the SSD for new writes, then rsync nightly from cache to spinning drives. The community tool &lt;code&gt;mergerfs.cache.tool&lt;/code&gt; (from the &lt;code&gt;trapexit/mergerfs-tools&lt;/code&gt; repo) automates the move.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-fstab" data-lang="fstab"&gt;# Two pools: one for writes (SSD-first), one for reads (everything).
/mnt/cache:/mnt/pool/disk* /mnt/writes fuse.mergerfs defaults,allow_other,use_ino,category.create=ff,minfreespace=20G,fsname=mergerfs-writes,nofail 0 0
/mnt/cache:/mnt/pool/disk* /mnt/storage fuse.mergerfs defaults,allow_other,use_ino,category.create=epmfs,minfreespace=50G,fsname=mergerfs,nofail 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then schedule the move with cron:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/cron.d/mergerfs-cache&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; * * * root /usr/bin/mergerfs.cache.tool -m /mnt/cache -p /mnt/pool/disk -t &lt;span class="m"&gt;80&lt;/span&gt; &amp;gt;&amp;gt; /var/log/mergerfs-cache.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;-t 80&lt;/code&gt; flag tells the tool to drain the cache when it crosses 80% full. Flag names and behavior have shifted across mergerfs-tools releases, so run &lt;code&gt;mergerfs.cache.tool --help&lt;/code&gt; against your installed build before trusting that exact line. For most homelabs this is over-engineering. For a server that ingests a lot of new content during the day and wants the spinning disks parked, it earns its keep.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re still planning the chassis, port count and drive bays drive every later decision. An eight-bay case with room for an HBA gives you headroom for two or three drive generations of growth.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DP3PQFKW"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/rosewill-helium_hu_5126aadb3fcc1746.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/rosewill-helium_hu_69bfd459d0bfd04c.webp" width="600" height="898" alt="Rosewill Helium NAS ATX Mid-Tower Case" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A budget ATX mid-tower built with storage in mind. 10 drive bays, full ATX board support, and room for HBAs and SATA expanders. The right enclosure for a DIY NAS that needs to grow beyond 4-6 drives without moving to expensive rackmount chassis.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0g6YNIYR" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="snapraid-the-parity-layer-mergerfs-does-not-provide"&gt;SnapRAID: The Parity Layer MergerFS Does Not Provide
&lt;/h2&gt;&lt;p&gt;MergerFS by itself protects you from nothing. Lose a disk, lose its files. SnapRAID adds parity, checksums, and silent-corruption detection on a schedule, which fits write-once-read-many media perfectly.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
&lt;strong&gt;Important, read this before you configure anything&lt;/strong&gt;: SnapRAID parity is a snapshot, not real-time. Anything written or changed since the last sync is unprotected. If you import 500GB this afternoon and a disk dies before tonight&amp;rsquo;s sync runs, those 500GB are gone. Plan your sync cadence and your manual-sync habits with that in mind, then read on.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install snapraid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Configure &lt;code&gt;/etc/snapraid.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Parity disk(s): must be at least as large as your largest data disk.
# NOT part of the MergerFS pool.
parity /mnt/pool/parity1/snapraid.parity
# Content files: SnapRAID writes the file list and checksums here.
# Keep one copy off the data disks plus one on each data disk for redundancy.
# If a data disk dies, you still have a content file on the surviving disks.
content /var/snapraid/snapraid.content
content /mnt/pool/disk1/snapraid.content
content /mnt/pool/disk2/snapraid.content
# Data disks: point to the underlying mounts, NOT the pool path.
# SnapRAID needs the raw filesystem to compute parity correctly.
data d1 /mnt/pool/disk1
data d2 /mnt/pool/disk2
data d3 /mnt/pool/disk3
# Excludes: skip transient files and the downloads branch.
exclude *.unrecoverable
exclude /tmp/
exclude /lost+found/
exclude downloads/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The parity disk is &lt;strong&gt;not&lt;/strong&gt; part of the MergerFS pool. It sits separately, dedicated to parity, and must be at least as large as your largest data disk. The &lt;code&gt;data d1 /mnt/pool/disk1&lt;/code&gt; lines point to the source disks rather than the pool, because SnapRAID needs to see the underlying filesystem to compute parity correctly.&lt;/p&gt;
&lt;p&gt;Initial sync:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid sync
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This builds the parity file. On a multi-TB library, expect this to take many hours. Schedule a regular sync + scrub via cron or a systemd timer. The community wrapper &lt;code&gt;snapraid-runner&lt;/code&gt; (Python) handles &amp;ldquo;sync, then scrub a percentage, then email me&amp;rdquo; with one config file. That is what I run nightly.&lt;/p&gt;
&lt;p&gt;When a drive dies, you replace it, mount the new disk at the same path, and run &lt;code&gt;snapraid fix -d d2&lt;/code&gt; to rebuild only the dead disk&amp;rsquo;s contents. No array reshape. No rebuild stress on the surviving disks.&lt;/p&gt;
&lt;div class="alert alert-default"&gt;
&lt;span class="alert-icon"&gt;ℹ️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Info:&lt;/strong&gt;
Run a manual &lt;code&gt;snapraid sync&lt;/code&gt; after big imports. The cron job covers steady-state. The night you dump a 4TB archive onto the pool needs a manual sync of its own.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="upgrading-and-replacing-drives-without-rebuilds"&gt;Upgrading and Replacing Drives Without Rebuilds
&lt;/h2&gt;&lt;p&gt;This is the killer feature, and it&amp;rsquo;s genuinely as simple as advertised.&lt;/p&gt;
&lt;h3 id="adding-a-new-drive"&gt;Adding a new drive
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 0. Check the new drive&amp;#39;s health before you trust it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smartctl -a /dev/sde
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Partition, format, mount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo parted /dev/sde -- mklabel gpt mkpart primary ext4 0% 100%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkfs.ext4 -L disk5 /dev/sde1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir /mnt/pool/disk5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo blkid /dev/sde1 &lt;span class="c1"&gt;# grab UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Add to fstab (same pattern as the other disks)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;UUID=newuuid /mnt/pool/disk5 ext4 defaults,noatime,nofail 0 2&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /mnt/pool/disk5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 3. If using epmfs, mirror your top-level folders so files route correctly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/pool/disk5/&lt;span class="o"&gt;{&lt;/span&gt;movies,tv,music&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown media:media /mnt/pool/disk5/&lt;span class="o"&gt;{&lt;/span&gt;movies,tv,music&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 4. Remount the pool to pick up the new branch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 5. Add it to snapraid.conf as data d5 /mnt/pool/disk5, then sync&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid sync
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Total downtime: a few seconds during the umount/mount. New writes start landing on the new drive immediately because it has the most free space.&lt;/p&gt;
&lt;h3 id="replacing-a-failing-drive"&gt;Replacing a failing drive
&lt;/h3&gt;&lt;p&gt;If &lt;code&gt;smartctl&lt;/code&gt; is screaming and the disk is still readable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Mount new disk at /mnt/pool/disk2-new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. rsync the dying disk to it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo rsync -aHAX --info&lt;span class="o"&gt;=&lt;/span&gt;progress2 /mnt/pool/disk2/ /mnt/pool/disk2-new/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 3. Unmount old, mount new at the same path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 4. Update fstab UUID, sudo mount /mnt/pool/disk2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 5. snapraid sync (parity is unchanged because data is the same)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the disk is dead and unreadable, skip the rsync and let SnapRAID rebuild instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Replace physical disk, format, mount at /mnt/pool/disk2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Run snapraid fix to rebuild that disk&amp;#39;s contents from parity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid -d d2 -l fix.log fix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid -d d2 -l check.log check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid sync
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the moment when SnapRAID earns its keep. Without it, those files are gone.&lt;/p&gt;
&lt;p&gt;One more piece you&amp;rsquo;ll want before you start swapping drives in earnest: the right cable between your HBA and the bays.&lt;/p&gt;
&lt;div class="product-box" data-asin="B08C2LJBLW"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/sff-8643_hu_6a88a92869df4f25.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/sff-8643_hu_935e8172ad15252c.webp" width="600" height="600" alt="SFF-8643 to 4x SATA" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A Mini-SAS &amp;ldquo;forward breakout&amp;rdquo; cable that lets one HBA/controller SFF-8643 port connect to four individual SATA drives. Use this when your HBA has an internal Mini-SAS port. It isn&amp;rsquo;t the &amp;ldquo;reverse breakout&amp;rdquo; cable for feeding a backplane from motherboard SATA, so check which direction you need before ordering.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4jkEr9h" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-common-mergerfs-failures"&gt;Troubleshooting Common MergerFS Failures
&lt;/h2&gt;&lt;h3 id="permission-denied-or-no-such-file-from-docker"&gt;&amp;ldquo;Permission denied&amp;rdquo; or &amp;ldquo;no such file&amp;rdquo; from Docker
&lt;/h3&gt;&lt;p&gt;You forgot &lt;code&gt;allow_other&lt;/code&gt;. Add it to the fstab options, remount, restart the container. If you still see denials, check that the container&amp;rsquo;s PUID/PGID match the owner of files in the pool.&lt;/p&gt;
&lt;h3 id="pool-shows-as-empty-after-reboot"&gt;Pool shows as empty after reboot
&lt;/h3&gt;&lt;p&gt;The pool mounted before the source disks did. Add &lt;code&gt;x-systemd.after=/mnt/pool/diskN&lt;/code&gt; for &lt;strong&gt;every&lt;/strong&gt; required disk to the fstab options, run &lt;code&gt;systemctl daemon-reload&lt;/code&gt;, reboot to verify. If you only listed disk1 and disk1 is the slow or dead one, you will reproduce the bug.&lt;/p&gt;
&lt;h3 id="df-shows-weird-sizes"&gt;&lt;code&gt;df&lt;/code&gt; shows weird sizes
&lt;/h3&gt;&lt;p&gt;If your pool is at &lt;code&gt;/mnt/storage&lt;/code&gt; and a source disk is also visible inside the pool path (a common typo: &lt;code&gt;/mnt/pool/disk*&lt;/code&gt; matching &lt;code&gt;/mnt/pool/disk-old-backup&lt;/code&gt;), you&amp;rsquo;ll see double-counted space. Make sure your wildcard only matches active source disks.&lt;/p&gt;
&lt;h3 id="slow-writes-on-large-files"&gt;Slow writes on large files
&lt;/h3&gt;&lt;p&gt;Try &lt;code&gt;direct_io,noforget&lt;/code&gt; instead of &lt;code&gt;cache.files=off,dropcacheonclose=true&lt;/code&gt;. &lt;code&gt;direct_io&lt;/code&gt; bypasses the page cache entirely on the FUSE side, which on modern drives (200MB/s+ sequential) is usually a wash or slight win. &lt;code&gt;noforget&lt;/code&gt; reduces inode churn.&lt;/p&gt;
&lt;h3 id="stale-file-handle-after-long-running-operations"&gt;&amp;ldquo;Stale file handle&amp;rdquo; after long-running operations
&lt;/h3&gt;&lt;p&gt;Almost always a &lt;code&gt;use_ino&lt;/code&gt; issue. Make sure it&amp;rsquo;s set. If the problem persists, check that the underlying filesystems all support consistent inodes (ext4, XFS yes; some FAT variants no).&lt;/p&gt;
&lt;h3 id="snapraid-sync-warns-about-too-many-changed-files"&gt;SnapRAID sync warns about too many changed files
&lt;/h3&gt;&lt;p&gt;SnapRAID has a default threshold for how many deletes/moves it&amp;rsquo;ll accept without a &lt;code&gt;--force-empty&lt;/code&gt; or similar flag. If you reorganized your library, this is correct, paranoid behavior. Read the warning, confirm it matches what you actually did, then run with the suggested flag.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I add a new drive to MergerFS without losing data or rebuilding?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Partition and format the new drive with ext4 or XFS, add it to /etc/fstab using a label or UUID, and mount it at the next sequential path like /mnt/pool/disk5. Because your pool source is a glob (/mnt/pool/disk*), MergerFS picks up the new branch on the next pool remount. No rebuild, no downtime beyond the brief unmount.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use mfs or epmfs for a media server with separate movies and TV folders?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use epmfs if you want files routed to the disk that already contains the matching top-level folder (so all of one show stays together). Use mfs if your library is flat or you don&amp;rsquo;t care about per-disk locality. epmfs gives you cleaner failure domains; mfs gives you simpler balancing. Both are fine; pick one and stick with it.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does MergerFS work with existing data on drives, and how does it handle mismatched sizes?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Drives can already hold data when you add them to the pool; MergerFS exposes whatever is there and unifies the namespace. Mismatched sizes are a core design assumption. A 4TB and a 14TB drive contribute their actual capacity, and create policies like mfs and pfrd handle the asymmetry without favoritism.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why pair MergerFS with SnapRAID instead of using ZFS or RAID?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;ZFS and RAID strip away the things that make MergerFS attractive for media: mismatched drive sizes, drive-by-drive expansion, and per-disk failure isolation. SnapRAID adds parity and silent-corruption detection without forcing you to give up any of those wins. For write-once-read-many media libraries on commodity drives, the combination is hard to beat.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What happens if a drive fails in MergerFS, do I lose everything like RAID5?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Each drive is its own filesystem with its own files. A dead drive takes only its own contents with it; the rest of the pool keeps serving normally. With SnapRAID layered on, you can rebuild the dead drive&amp;rsquo;s contents from parity onto a replacement. Without SnapRAID, you lose only that drive&amp;rsquo;s files and the rest of the pool keeps serving.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;MergerFS solves a specific problem: pooling commodity drives into one mount for a Linux media server, without inheriting RAID&amp;rsquo;s brittleness or buying matched-drive sets you don&amp;rsquo;t need. Pair it with SnapRAID for parity, set &lt;code&gt;category.create=mfs&lt;/code&gt; or &lt;code&gt;epmfs&lt;/code&gt; based on your library layout, lock down the fstab options (&lt;code&gt;allow_other&lt;/code&gt;, &lt;code&gt;use_ino&lt;/code&gt;, &lt;code&gt;cache.files=off&lt;/code&gt;, &lt;code&gt;minfreespace=50G&lt;/code&gt;, &lt;code&gt;nofail&lt;/code&gt;, and an &lt;code&gt;x-systemd.requires&lt;/code&gt; entry per source disk), and use the same UID/GID across host and containers. That stack will outlive several drive generations.&lt;/p&gt;
&lt;p&gt;Next steps if you&amp;rsquo;re building this out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read trapexit&amp;rsquo;s official docs at &lt;a class="link" href="https://github.com/trapexit/mergerfs/releases" target="_blank" rel="noopener"
&gt;GitHub mergerfs releases&lt;/a&gt; for the full list of policies and runtime options.&lt;/li&gt;
&lt;li&gt;For ongoing health monitoring, run &lt;code&gt;smartctl&lt;/code&gt; checks on each source disk via a weekly cron and a &lt;code&gt;snapraid scrub&lt;/code&gt; on 5-10% of the pool nightly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Build your own NAS storage that grows with you. The first time you swap a 4TB for a 14TB without anything noticing, you&amp;rsquo;ll understand why the unraid and jbod approach has stuck around.&lt;/p&gt;</description></item><item><title>Mount a Network Drive in Proxmox LXC the Right Way</title><link>https://diymediaserver.com/post/2026/proxmox-lxc-containers/</link><pubDate>Thu, 23 Apr 2026 06:14:52 -0600</pubDate><guid>https://diymediaserver.com/post/2026/proxmox-lxc-containers/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/proxmox-lxc-containers/featured_hu_7cd25e5b3c937783.webp" alt="Featured image of post Mount a Network Drive in Proxmox LXC the Right Way" /&gt;&lt;p&gt;If you&amp;rsquo;re running a homelab on Proxmox and still spinning up full VMs for every little service, you&amp;rsquo;re burning RAM for no reason.&lt;/p&gt;
&lt;p&gt;LXC containers give you near bare-metal performance for a fraction of the overhead. For Pi-hole, Nginx, Jellyfin, Sonarr, or qBittorrent, they sit in the sweet spot between &amp;ldquo;full VM&amp;rdquo; and &amp;ldquo;Docker everywhere.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Then you try to mount your network share into an LXC. Files show up owned by &lt;code&gt;nobody&lt;/code&gt;. Sonarr refuses to move media. Jellyfin logs fill with permission errors.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t ask me how I know. I migrated half my stack from VMs to LXCs to &amp;ldquo;save resources&amp;rdquo; without really understanding UID mapping. That afternoon was my crash course in how unprivileged containers work.&lt;/p&gt;
&lt;p&gt;This guide will save you that afternoon.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When to choose LXC vs VM&lt;/li&gt;
&lt;li&gt;Privileged vs unprivileged containers&lt;/li&gt;
&lt;li&gt;UID/GID mapping basics&lt;/li&gt;
&lt;li&gt;How to mount a network share in Proxmox the right way&lt;/li&gt;
&lt;li&gt;Making mounts persistent across reboots&lt;/li&gt;
&lt;li&gt;Bind mounts vs NFS vs SMB tradeoffs&lt;/li&gt;
&lt;li&gt;Backups, snapshots, and moving containers&lt;/li&gt;
&lt;li&gt;How to move an LXC from external drive to local Proxmox storage safely&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Use unprivileged LXC containers for lightweight services on Proxmox. Mount storage on the host and bind it into the container. Understand UID/GID mapping before you touch your NAS, or you&amp;rsquo;ll spend an afternoon chasing permission errors that were never really permission errors.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="when-to-use-an-lxc-instead-of-a-vm"&gt;When to Use an LXC Instead of a VM
&lt;/h2&gt;&lt;p&gt;LXC containers use OS-level virtualization. They share the host kernel. That makes them lightweight and fast compared to full VMs.&lt;/p&gt;
&lt;p&gt;For most homelab services, LXC is the right call. Use one when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re running a single service: Pi-hole, Nginx, Jellyfin, qBittorrent, Home Assistant&lt;/li&gt;
&lt;li&gt;You want low RAM overhead and faster startup&lt;/li&gt;
&lt;li&gt;You want tight Proxmox integration and easy &lt;code&gt;vzdump&lt;/code&gt; backups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use a VM when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need Windows&lt;/li&gt;
&lt;li&gt;You need full kernel isolation&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re doing GPU passthrough with proprietary drivers&lt;/li&gt;
&lt;li&gt;You want to run a full Docker Compose stack cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Running Docker inside a VM is still the cleanest approach for complex multi-container stacks. Docker inside LXC works, but it adds friction and security tradeoffs you&amp;rsquo;ll regret the first time something breaks.&lt;/p&gt;
&lt;p&gt;One more option worth knowing: Proxmox VE 9.1 introduced native OCI container support, which lets you run Docker-style images without a full Docker VM in some scenarios. Worth exploring if you&amp;rsquo;re on 9.1 or later.&lt;/p&gt;
&lt;h2 id="privileged-vs-unprivileged-containers"&gt;Privileged vs Unprivileged Containers
&lt;/h2&gt;&lt;p&gt;This is where most people get burned.&lt;/p&gt;
&lt;h3 id="unprivileged-lxc-default-recommended"&gt;Unprivileged LXC (Default, Recommended)
&lt;/h3&gt;&lt;p&gt;By default, Proxmox creates unprivileged containers.&lt;/p&gt;
&lt;p&gt;Inside the container, root is UID 0. On the host, that root maps to UID 100000. Container UIDs 0 through 65535 map to host UIDs 100000 through 165535.&lt;/p&gt;
&lt;p&gt;This user namespace mapping dramatically improves security. If the container is compromised, the attacker doesn&amp;rsquo;t get real root on the host. They get a high-numbered unprivileged user that can&amp;rsquo;t do much.&lt;/p&gt;
&lt;p&gt;This is what you want 90% of the time.&lt;/p&gt;
&lt;h3 id="privileged-lxc"&gt;Privileged LXC
&lt;/h3&gt;&lt;p&gt;Privileged containers use real host UIDs. Root inside the container equals root on the host. No UID remapping.&lt;/p&gt;
&lt;p&gt;That means fewer permission headaches and sometimes easier hardware passthrough or Docker-in-LXC. It also means weaker isolation and a much bigger blast radius if something goes sideways.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t run privileged unless you have a concrete reason. &amp;ldquo;It&amp;rsquo;s easier&amp;rdquo; is not a concrete reason.&lt;/p&gt;
&lt;h2 id="understanding-uidgid-mapping-without-losing-your-mind"&gt;Understanding UID/GID Mapping Without Losing Your Mind
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the core issue.&lt;/p&gt;
&lt;p&gt;Inside your LXC, your media app runs as UID 1000. On the host, that same UID 1000 inside the container shows up as UID 101000. The math is simple: 100000 + 1000.&lt;/p&gt;
&lt;p&gt;When you bind mount &lt;code&gt;/mnt/media&lt;/code&gt; from the host into the container, the container expects to see UID 1000. The host actually sees 101000.&lt;/p&gt;
&lt;p&gt;If your NAS files are owned by UID 1000 on the host, the container sees a mismatch. Result: &lt;code&gt;Permission denied&lt;/code&gt;, files owned by &lt;code&gt;nobody&lt;/code&gt;, and apps that quietly refuse to move or rename anything.&lt;/p&gt;
&lt;h3 id="the-safe-default-mapping"&gt;The Safe Default Mapping
&lt;/h3&gt;&lt;p&gt;Most Proxmox installs use:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc.idmap: u 0 100000 65536
lxc.idmap: g 0 100000 65536
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Confirm with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct config &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or test inside the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;CTID&amp;gt; -- id
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see high-number UIDs on mounted files, the mapping is doing its job. If you see UIDs that look &amp;ldquo;normal&amp;rdquo; (under 1000), you&amp;rsquo;re either looking at a privileged container or a custom idmap. Either way, find out which before you start running &lt;code&gt;chown&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-right-way-to-mount-a-synology-drive-in-proxmox"&gt;The Right Way to Mount a Synology Drive in Proxmox
&lt;/h2&gt;&lt;p&gt;This is the most important section if you&amp;rsquo;re building a media server. Read it twice.&lt;/p&gt;
&lt;h3 id="step-1-mount-synology-on-the-proxmox-host"&gt;Step 1: Mount Synology on the Proxmox Host
&lt;/h3&gt;&lt;p&gt;Don&amp;rsquo;t mount NFS or SMB directly inside the container. Mount on the host.&lt;/p&gt;
&lt;p&gt;Mount it on the host first. That gives you one place to manage permissions and makes troubleshooting far easier. If five containers all mount the same share independently, you get to debug five different permission problems.&lt;/p&gt;
&lt;p&gt;First, make sure the mount point exists and the right tools are installed.&lt;/p&gt;
&lt;p&gt;For NFS (recommended for performance):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install nfs-common
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -t nfs 192.168.1.10:/volume1/media /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For SMB from Synology:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install cifs-utils
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -t cifs //192.168.1.10/media /mnt/synology -o &lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user,password&lt;span class="o"&gt;=&lt;/span&gt;pass,vers&lt;span class="o"&gt;=&lt;/span&gt;3.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If ownership looks wrong here on the host, fix it before touching the LXC. This single check catches most issues before they spread into the container.&lt;/p&gt;
&lt;h3 id="making-mounts-persistent-across-reboots"&gt;Making Mounts Persistent Across Reboots
&lt;/h3&gt;&lt;p&gt;Manual &lt;code&gt;mount&lt;/code&gt; commands don&amp;rsquo;t survive a reboot. You need &lt;code&gt;/etc/fstab&lt;/code&gt; entries on the Proxmox host.&lt;/p&gt;
&lt;p&gt;For NFS:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.1.10:/volume1/media /mnt/synology nfs defaults,_netdev 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For SMB, use a credentials file. Plaintext passwords in &lt;code&gt;/etc/fstab&lt;/code&gt; are a bad idea on a system multiple people might log into.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;//192.168.1.10/media /mnt/synology cifs credentials=/etc/samba/synology.creds,vers=3.0,_netdev 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Create the credentials file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /etc/samba
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat &amp;gt; /etc/samba/synology.creds &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;username=user
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;password=yourpassword
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; /etc/samba/synology.creds
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then test before rebooting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;umount /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it mounts cleanly, you&amp;rsquo;re set. If it doesn&amp;rsquo;t, fix it now. A broken &lt;code&gt;fstab&lt;/code&gt; entry can stall the host at boot and force you into the rescue console, which is not how you want to spend your evening.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;_netdev&lt;/code&gt; flag tells the system to wait for the network before mounting. That matters on Proxmox hosts that finish booting before all interfaces are fully ready.&lt;/p&gt;
&lt;h3 id="step-2-bind-mount-into-lxc"&gt;Step 2: Bind Mount Into LXC
&lt;/h3&gt;&lt;p&gt;Now bind mount the host path into the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;CTID&amp;gt; -mp0 /mnt/synology,mp&lt;span class="o"&gt;=&lt;/span&gt;/mnt/media,acl&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few things to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mp0&lt;/code&gt; is the first mount point. If you already have an &lt;code&gt;mp0&lt;/code&gt;, this overwrites it. Use &lt;code&gt;mp1&lt;/code&gt;, &lt;code&gt;mp2&lt;/code&gt;, etc. for additional bind mounts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acl=1&lt;/code&gt; enables POSIX ACLs on the mount, which most media apps quietly want.&lt;/li&gt;
&lt;li&gt;Mount points don&amp;rsquo;t apply until the container restarts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reboot the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct reboot &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then check inside:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;CTID&amp;gt; -- ls -l /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see UIDs like 101000 or similar, that&amp;rsquo;s the mapping doing its job. If you see &lt;code&gt;nobody&lt;/code&gt; or strange ownership, jump to the next section.&lt;/p&gt;
&lt;h2 id="fixing-permission-denied-errors-on-nas-mounts"&gt;Fixing Permission Denied Errors on NAS Mounts
&lt;/h2&gt;&lt;p&gt;This is the most common issue with Plex, Jellyfin, and Sonarr. Almost every &amp;ldquo;my LXC can&amp;rsquo;t write to my NAS&amp;rdquo; thread on r/homelab ends here.&lt;/p&gt;
&lt;h3 id="option-1-adjust-ownership-on-host"&gt;Option 1: Adjust Ownership on Host
&lt;/h3&gt;&lt;p&gt;If your app runs as UID 1000 inside the container, host ownership must be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;100000 + 1000 = 101000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Read the warnings below before running this.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R 101000:101000 /mnt/synology
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;NFS warning:&lt;/strong&gt; Whether &lt;code&gt;chown&lt;/code&gt; actually does anything depends entirely on your Synology&amp;rsquo;s NFS export configuration. If the export has &lt;code&gt;root_squash&lt;/code&gt; enabled (Synology&amp;rsquo;s default), the Proxmox host can&amp;rsquo;t change file ownership on the NAS. The command appears to succeed. The underlying ownership stays exactly where it was. You&amp;rsquo;ll only notice when you check from another host or from DSM and see nothing changed.&lt;/p&gt;
&lt;p&gt;To make &lt;code&gt;chown&lt;/code&gt; work over NFS, you need either &lt;code&gt;no_root_squash&lt;/code&gt; on the Synology export, or you set &lt;code&gt;anonuid&lt;/code&gt; and &lt;code&gt;anongid&lt;/code&gt; to the expected host-side UID (101000 in this case). On the Synology, that&amp;rsquo;s Control Panel &amp;gt; Shared Folder &amp;gt; Edit &amp;gt; NFS Permissions. Check which squash setting is active before you blame Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMB warning:&lt;/strong&gt; &lt;code&gt;chown&lt;/code&gt; does nothing on SMB mounts. Permissions on SMB are controlled by the share configuration on the Synology, not by Linux ownership. Set the Synology user permissions on the share itself.&lt;/p&gt;
&lt;h3 id="option-2-custom-lxcidmap-advanced"&gt;Option 2: Custom lxc.idmap (Advanced)
&lt;/h3&gt;&lt;p&gt;Sometimes you need a specific NAS UID to map cleanly into the container. Say your Synology uses UID 1026 for the media user, and you want UID 1026 inside the container to match UID 1026 on the host.&lt;/p&gt;
&lt;p&gt;Edit &lt;code&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt; and add a custom mapping that carves UID 1026 out of the normal offset:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc.idmap: u 0 100000 1026
lxc.idmap: u 1026 1026 1
lxc.idmap: u 1027 101027 64509
lxc.idmap: g 0 100000 1026
lxc.idmap: g 1026 1026 1
lxc.idmap: g 1027 101027 64509
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This passes UID 1026 through directly. Everything else uses the normal 100000 offset.&lt;/p&gt;
&lt;p&gt;You also need to authorize the carved-out UID and GID on the host, or the container will refuse to start. Add these lines to &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; respectively:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;root:1026:1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Confirm the default offset on your host before trusting any of this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep root /etc/subuid /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see something like &lt;code&gt;root:100000:65536&lt;/code&gt;. If yours is different, adjust the numbers above to match.&lt;/p&gt;
&lt;p&gt;This is genuinely advanced and easy to break. The container won&amp;rsquo;t even start if the subuid/subgid lines are missing. Unless you have a concrete reason for it, adjusting ownership to match the default mapping is the safer path.&lt;/p&gt;
&lt;h2 id="bind-mount-vs-nfs-vs-smb-what-should-you-use"&gt;Bind Mount vs NFS vs SMB: What Should You Use?
&lt;/h2&gt;&lt;p&gt;For media workloads: mount on the host, bind mount into the LXC.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bind mount on host:&lt;/strong&gt; Lowest overhead, best performance, single point to manage permissions. This is the right choice for media servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS directly in container:&lt;/strong&gt; Possible. Adds network overhead, creates more permission complexity, and is harder to manage centrally. Skip it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMB:&lt;/strong&gt; Higher latency, more permission quirks, more fragile for Linux services. Use it only if NFS isn&amp;rsquo;t an option on your NAS.&lt;/p&gt;
&lt;h2 id="where-should-you-store-lxc-root-filesystems"&gt;Where Should You Store LXC Root Filesystems?
&lt;/h2&gt;&lt;p&gt;You can store LXC rootfs on &lt;code&gt;local-lvm&lt;/code&gt;, &lt;code&gt;local-zfs&lt;/code&gt;, or the boot storage.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t put everything on the boot drive. If your boot drive is also handling heavy media IO, host performance suffers and so does anything else running on it.&lt;/p&gt;
&lt;p&gt;A cleaner layout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OS on boot SSD&lt;/li&gt;
&lt;li&gt;Containers on a dedicated ZFS pool or SSD-backed storage&lt;/li&gt;
&lt;li&gt;Media on a separate pool or NAS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep rootfs small and fast. Put bulk data on larger storage. Avoid booting Proxmox from a USB stick if you can. Proxmox writes logs and cluster state to the boot device constantly, and cheap flash wears out fast under that kind of load.&lt;/p&gt;
&lt;h2 id="backups-and-snapshots"&gt;Backups and Snapshots
&lt;/h2&gt;&lt;p&gt;One of the biggest advantages of LXC in Proxmox is native backup support. Use it.&lt;/p&gt;
&lt;h3 id="backup"&gt;Backup
&lt;/h3&gt;&lt;p&gt;Via GUI: Datacenter &amp;gt; Backup &amp;gt; Add Job. Make sure mount points are included if &lt;code&gt;backup=1&lt;/code&gt; is set on the bind mount. By default, bind mounts are skipped.&lt;/p&gt;
&lt;p&gt;CLI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vzdump &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="snapshots"&gt;Snapshots
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct snapshot &amp;lt;CTID&amp;gt; pre-update --description &lt;span class="s2"&gt;&amp;#34;Before upgrade&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With ZFS, snapshots are near-instant and cost almost nothing in disk space. Take one before every upgrade. You will eventually need to roll one back, and you&amp;rsquo;ll be glad you did.&lt;/p&gt;
&lt;p&gt;Important: bind-mounted directories are not snapshotted. Your media directory on a NAS won&amp;rsquo;t be included in a Proxmox snapshot or backup automatically. Plan accordingly. Don&amp;rsquo;t assume &lt;code&gt;vzdump&lt;/code&gt; covers everything, because it won&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="troubleshooting-common-lxc-storage-issues"&gt;Troubleshooting Common LXC Storage Issues
&lt;/h2&gt;&lt;h3 id="1-files-show-as-owned-by-nobody"&gt;1. Files Show as Owned by &lt;code&gt;nobody&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Cause: UID mismatch due to unprivileged mapping.&lt;/p&gt;
&lt;p&gt;Fix: Adjust host ownership to the mapped UID (typically 101000 for app UID 1000). Check Synology NFS export settings first, specifically &lt;code&gt;root_squash&lt;/code&gt;, or your &lt;code&gt;chown&lt;/code&gt; will lie to you.&lt;/p&gt;
&lt;h3 id="2-sonarr-cannot-move-files"&gt;2. Sonarr Cannot Move Files
&lt;/h3&gt;&lt;p&gt;Cause: Download folder and media folder are owned by different mapped IDs, or they&amp;rsquo;re on different filesystems entirely (which forces a slow copy and can fail on permissions).&lt;/p&gt;
&lt;p&gt;Fix: Ensure both paths are owned by the same UID and GID on the host, and that they live on the same mount when possible.&lt;/p&gt;
&lt;h3 id="3-lxc-cannot-see-proxmox-smb-share"&gt;3. LXC Cannot See Proxmox SMB Share
&lt;/h3&gt;&lt;p&gt;Cause: SMB mounted inside the container instead of on the host. Or &lt;code&gt;cifs-utils&lt;/code&gt; is missing on the host.&lt;/p&gt;
&lt;p&gt;Fix: Mount on host. Bind mount into container. Install &lt;code&gt;cifs-utils&lt;/code&gt; on the host, not the LXC.&lt;/p&gt;
&lt;h3 id="4-high-io-or-slow-streaming"&gt;4. High IO or Slow Streaming
&lt;/h3&gt;&lt;p&gt;Cause: Using SMB instead of NFS. Running heavy IO on the boot pool.&lt;/p&gt;
&lt;p&gt;Fix: Switch to NFS. Move container rootfs to a dedicated pool. If transcoding is the bottleneck, a CPU with Quick Sync will offload most of it.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A 14-core (6 P-cores plus 8 E-cores), 20-thread Raptor Lake Refresh CPU with strong single-thread performance and UHD 770 graphics, giving you Quick Sync hardware transcoding for Jellyfin/Plex. Ideal for Proxmox homelabs and everyday desktops when you want snappy VM/container workloads without needing a discrete GPU.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="5-permission-denied-after-restore"&gt;5. Permission Denied After Restore
&lt;/h3&gt;&lt;p&gt;Cause: Restored to a different host with different UID mapping.&lt;/p&gt;
&lt;p&gt;Fix: Verify &lt;code&gt;lxc.idmap&lt;/code&gt; lines match the original system. Adjust ownership as needed. This bites people who restore a backup to a fresh Proxmox install and forget that the host UID offsets have to line up.&lt;/p&gt;
&lt;h3 id="6-chown-does-nothing-on-synology-nfs-mount"&gt;6. chown Does Nothing on Synology NFS Mount
&lt;/h3&gt;&lt;p&gt;Cause: Synology NFS export has &lt;code&gt;root_squash&lt;/code&gt; enabled. The host can&amp;rsquo;t change file ownership on the NAS.&lt;/p&gt;
&lt;p&gt;Fix: Adjust NFS export settings on the Synology (Control Panel &amp;gt; Shared Folder &amp;gt; NFS Permissions). Use &lt;code&gt;anonuid&lt;/code&gt;/&lt;code&gt;anongid&lt;/code&gt; to control how the NAS presents ownership to Proxmox, or switch to &lt;code&gt;no_root_squash&lt;/code&gt; in a controlled environment. Don&amp;rsquo;t enable &lt;code&gt;no_root_squash&lt;/code&gt; on a share that&amp;rsquo;s reachable from anything you don&amp;rsquo;t trust.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I fix permission denied errors when mounting a Synology NFS share into a Proxmox LXC for Plex or Jellyfin?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Mount NFS on the Proxmox host first, not inside the container. Bind mount it into the LXC. Check your Synology NFS export settings, specifically whether &lt;code&gt;root_squash&lt;/code&gt; is enabled. If it is, &lt;code&gt;chown&lt;/code&gt; won&amp;rsquo;t change ownership on the NAS side. Set &lt;code&gt;anonuid&lt;/code&gt;/&lt;code&gt;anongid&lt;/code&gt; on the NFS export, or adjust squash settings on your Synology.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Privileged or unprivileged LXC for running Docker inside Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Unprivileged is safer. If you must run Docker in LXC, many people use privileged containers, which reduces isolation and isn&amp;rsquo;t ideal for internet-exposed workloads. Better option: use a VM for Docker Compose stacks, or explore Proxmox 9.1 native OCI support.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Best way to bind mount a ZFS pool or external drive without UID/GID issues?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;ol&gt;
&lt;li&gt;Mount on host.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;pct set &amp;lt;CTID&amp;gt; -mpX /host/path,mp=/container/path,acl=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adjust host ownership to match the mapped UID. Check NFS squash settings before you run &lt;code&gt;chown&lt;/code&gt; so you&amp;rsquo;re not chasing a ghost.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Avoid custom idmap unless you have a specific UID mismatch that can&amp;rsquo;t be solved by adjusting ownership.&lt;/p&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I store LXC rootfs on the Proxmox boot drive?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;You can. It&amp;rsquo;s not best practice for heavy workloads. Separate OS and container storage for better stability and performance, and your boot drive will last longer.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does Proxmox LXC support snapshots of bind-mounted media directories?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Snapshots cover the container rootfs only. External bind mounts must be backed up separately, typically with ZFS snapshots on the underlying pool or with native NAS snapshots on the Synology side.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ LXC vs VM vs Docker VM for qBittorrent?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Single instance, lightweight: LXC. Full Docker stack with VPN and multiple services: VM with Docker. Need maximum isolation: VM.&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;If you&amp;rsquo;re sizing up new hardware for a Proxmox host that runs a dozen LXCs plus a Docker VM, network and storage I/O matter more than raw clock speed.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A compact mini-workstation built around up to a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. Great as a homelab node or small server with serious I/O.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="final-thoughts-mount-network-shares-correctly-or-pay-for-it-later"&gt;Final Thoughts: Mount Network Shares Correctly or Pay for It Later
&lt;/h2&gt;&lt;p&gt;Proxmox LXC containers are one of the best tools in a homelab. Lightweight, fast, integrated, easy to back up.&lt;/p&gt;
&lt;p&gt;But they&amp;rsquo;re not magic.&lt;/p&gt;
&lt;p&gt;The moment you mount a Synology drive in Proxmox or bind mount a ZFS dataset into an LXC, UID and GID mapping becomes real. Ignore NFS squash settings and you&amp;rsquo;ll spend an afternoon chasing phantom permission errors that aren&amp;rsquo;t really permission errors at all.&lt;/p&gt;
&lt;p&gt;If you remember only three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use unprivileged containers by default.&lt;/li&gt;
&lt;li&gt;Mount NAS storage on the host, then bind mount into LXC.&lt;/li&gt;
&lt;li&gt;Fix ownership on the host to match mapped UIDs, and check your Synology NFS export settings before you blame Proxmox.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do that and you&amp;rsquo;ll skip the wall of errors that ate my afternoon during my VM-to-LXC migration. You don&amp;rsquo;t need privileged containers and you don&amp;rsquo;t need custom idmap. You need to understand what the offset does and work with it instead of fighting it.&lt;/p&gt;
&lt;p&gt;If you want to go deeper next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explore Proxmox 9.1 OCI containers.&lt;/li&gt;
&lt;li&gt;Learn ZFS dataset tuning for media workloads.&lt;/li&gt;
&lt;li&gt;Build a clean storage layout separating OS, containers, and bulk data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if your current box is the bottleneck before you finish reading this, a small Proxmox-friendly node with fast networking will get you further than throwing more RAM at an aging tower.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has more than enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without turning your closet into a jet engine or a space heater.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>M5 MacBook Air vs Windows Laptop for Developers</title><link>https://diymediaserver.com/post/2026/m5-macbook-air-vs-windows-laptop-school-dev/</link><pubDate>Tue, 21 Apr 2026 14:53:29 -0600</pubDate><guid>https://diymediaserver.com/post/2026/m5-macbook-air-vs-windows-laptop-school-dev/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/m5-macbook-air-vs-windows-laptop-school-dev/featured_hu_f0064b3f43a7218e.webp" alt="Featured image of post M5 MacBook Air vs Windows Laptop for Developers" /&gt;&lt;p&gt;Going back to school as an adult changes how you look at your tools.&lt;/p&gt;
&lt;p&gt;I needed a laptop for coursework, software development, running my blog, and working while I travel. That means IDEs, Docker containers, SSH sessions into my homelab, dozens of browser tabs, and long stretches away from power outlets.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d been a Windows user for years. My desktop handled everything. Over time, every non-gaming task got worse. Browsing felt sluggish. Actual work became clunky and frustrating. Even checking email started to feel like I was managing an operating system instead of using one.&lt;/p&gt;
&lt;p&gt;So I bought a 13-inch M5 MacBook Air.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how I got there, what I learned, and what I&amp;rsquo;d tell you if you&amp;rsquo;re staring down the same macOS vs. Windows decision for school and development.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
I chose the M5 MacBook Air because it gives me silent performance, all-day battery, a Unix-native development environment, and a machine I can travel with. I kept my Windows desktop strictly for gaming. Each machine does what it&amp;rsquo;s good at. That&amp;rsquo;s the whole story.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-windows-finally-wore-me-down"&gt;Why Windows Finally Wore Me Down
&lt;/h2&gt;&lt;p&gt;My Windows desktop isn&amp;rsquo;t slow on paper. Strong CPU, plenty of RAM, capable GPU. But the OS experience degraded over time in ways that specs can&amp;rsquo;t capture.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what wore on me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Background processes spiking CPU at random. I never asked for &amp;ldquo;Microsoft Compatibility Telemetry&amp;rdquo; to eat 30% of a core, but there it was.&lt;/li&gt;
&lt;li&gt;Disk thrashing from indexing and update downloads, sometimes during active work sessions.&lt;/li&gt;
&lt;li&gt;Forced reboots at the worst possible moments. Mid-compile. Mid-deploy. Once, mid-exam.&lt;/li&gt;
&lt;li&gt;OEM bloatware on every Windows laptop I evaluated. Fresh installs help, but you shouldn&amp;rsquo;t need one on day one.&lt;/li&gt;
&lt;li&gt;A consistent feeling of fighting the machine instead of using it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hardware was fast. The OS felt slow.&lt;/p&gt;
&lt;p&gt;You know that moment when you open your laptop to check something quickly and it decides right now is the perfect time to finish installing updates at zero percent? That happened enough times to change my behavior. I started hesitating before reboots. I delayed updates. I kept too many things open to avoid restarting.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not a workflow. That&amp;rsquo;s damage control.&lt;/p&gt;
&lt;h2 id="microsoft-solved-the-reboot-problem-for-enterprise-customers"&gt;Microsoft Solved the Reboot Problem, For Enterprise Customers
&lt;/h2&gt;&lt;p&gt;Windows has always meant: security update equals restart.&lt;/p&gt;
&lt;p&gt;Microsoft introduced hotpatching, which applies certain security updates without requiring a reboot. That&amp;rsquo;s a genuine improvement.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the catch.&lt;/p&gt;
&lt;p&gt;Hotpatching is limited to Windows 11 Enterprise customers with specific Microsoft 365 subscriptions. Not Home. Not Pro. And Windows Server hotpatching now involves per-server subscription licensing.&lt;/p&gt;
&lt;p&gt;Microsoft solved the problem. Then put the solution behind Enterprise licensing.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re on Home or Pro, you&amp;rsquo;re still getting interrupted.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the pattern that finally broke me. Basic workflow stability became a premium feature. It felt like my time was worth less than an enterprise customer&amp;rsquo;s. Once you see that pattern, you can&amp;rsquo;t unsee it.&lt;/p&gt;
&lt;p&gt;Compare that with macOS: most updates don&amp;rsquo;t require a reboot at all. When they do, it&amp;rsquo;s scheduled and predictable. Nobody&amp;rsquo;s charging you extra to avoid being interrupted.&lt;/p&gt;
&lt;h2 id="what-i-needed"&gt;What I Needed
&lt;/h2&gt;&lt;p&gt;Before switching platforms, I wrote down requirements. Not wants. Needs. If you skip this step, you&amp;rsquo;ll end up buying for vibes instead of utility.&lt;/p&gt;
&lt;h3 id="school"&gt;School
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Office or Google Docs&lt;/li&gt;
&lt;li&gt;Secure browser for exams (Respondus, ExamSoft, etc.)&lt;/li&gt;
&lt;li&gt;All-day battery for long class days&lt;/li&gt;
&lt;li&gt;Quiet operation in lecture halls&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="development"&gt;Development
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;VS Code (or your editor of choice)&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;SSH into my homelab (Proxmox hosts, pfSense, NAS boxes)&lt;/li&gt;
&lt;li&gt;Light to moderate compiles&lt;/li&gt;
&lt;li&gt;Occasional Linux VMs for testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="writing-and-blogging"&gt;Writing and Blogging
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Multiple browser tabs (research, CMS, preview)&lt;/li&gt;
&lt;li&gt;Markdown editor&lt;/li&gt;
&lt;li&gt;Fast, responsive multitasking&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="travel"&gt;Travel
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Lightweight (under 3 lbs preferred)&lt;/li&gt;
&lt;li&gt;All-day battery&lt;/li&gt;
&lt;li&gt;Silent on planes&lt;/li&gt;
&lt;li&gt;USB-C charging (one charger for everything)&lt;/li&gt;
&lt;li&gt;Strong Wi-Fi&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once I wrote that out, the M5 MacBook Air stopped looking like a lifestyle choice and started looking like a serious tool match.&lt;/p&gt;
&lt;h2 id="built-for-life-on-the-road"&gt;Built for Life on the Road
&lt;/h2&gt;&lt;p&gt;Travel was a real factor in this decision.&lt;/p&gt;
&lt;p&gt;The 13-inch M5 Air weighs 2.7 pounds. Most Windows laptops at this price land closer to 4 to 5 pounds, especially anything with a dedicated GPU. That difference doesn&amp;rsquo;t sound like much until you&amp;rsquo;ve hauled it through three terminals and a connecting flight.&lt;/p&gt;
&lt;h3 id="fanless-design"&gt;Fanless Design
&lt;/h3&gt;&lt;p&gt;The M5 Air has no fan. Zero. For Office, browsing, and typical coding sessions, it runs silently without noticeable throttling. Apple Silicon&amp;rsquo;s efficiency cores handle idle and light tasks without spinning anything up.&lt;/p&gt;
&lt;p&gt;That matters more than you&amp;rsquo;d think:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Silent in hotel rooms when your partner is sleeping&lt;/li&gt;
&lt;li&gt;No fan noise on planes (your seatmate already hates you for existing, no need to add a whining fan)&lt;/li&gt;
&lt;li&gt;No heat on your lap during long flights&lt;/li&gt;
&lt;li&gt;No dust accumulating in the chassis over years of travel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For sustained heavy rendering or massive build pipelines running all day, a MacBook Pro with active cooling is the right call. For school and development work, the Air handles it without complaint.&lt;/p&gt;
&lt;h3 id="battery-life"&gt;Battery Life
&lt;/h3&gt;&lt;p&gt;Apple rates it at up to 18 hours. In real-world use with Wi-Fi, browser tabs, writing, and SSH sessions, expect a solid 10 to 14 hours depending on workload.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve gone through entire layovers without checking my battery percentage. That changes how you work. You stop hunting for outlets. You stop rationing screen brightness. You get on with it.&lt;/p&gt;
&lt;h3 id="wi-fi-7"&gt;Wi-Fi 7
&lt;/h3&gt;&lt;p&gt;The M5 Air includes Wi-Fi 7 (802.11be). In practice, faster channel negotiation and reduced latency help when pushing Git commits or running SSH sessions over congested hotel and airport networks. It&amp;rsquo;s not a dramatic day-to-day difference, but on a crowded network it&amp;rsquo;s noticeably snappier. You&amp;rsquo;ll need a Wi-Fi 7 access point at home to take full advantage.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0GR1BY1SZ"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/macbook_hu_2a1a011958cd5e86.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/macbook_hu_a386a7d25c9bf5a4.webp" width="600" height="364" alt="Apple 2026 MacBook Air 13-inch Laptop with M5 chip" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
This ultra-portable powerhouse features the latest Apple silicon paired with 24GB of unified memory and a massive 1TB SSD, delivering desktop-class performance for heavy multitasking and professional creative workflows. Its fanless, razor-thin design and brilliant 13.6-inch Liquid Retina display make it the ultimate mobile workstation for those who refuse to compromise on speed or storage.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/48MaPin" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="doing-the-budget-math"&gt;Doing the Budget Math
&lt;/h2&gt;&lt;p&gt;At roughly $1,400 you&amp;rsquo;re choosing between two very different machines.&lt;/p&gt;
&lt;p&gt;A Windows gaming laptop at that price gives you a strong CPU, dedicated GPU, loud fans, 4 to 6 hour real-world battery, and a heavier chassis. Great if you game on the go. I don&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;The M5 MacBook Air at the same price is fanless, silent, light, and has genuine all-day battery. Education pricing through Apple&amp;rsquo;s education store shaves off a bit more. Macs also hold resale value better than most Windows laptops, which matters if you&amp;rsquo;re planning to upgrade in a few years.&lt;/p&gt;
&lt;p&gt;For my workload, the Mac was the better fit at the same price. If gaming on the go is your priority, the Windows laptop wins. Know what you&amp;rsquo;re actually buying before you buy it.&lt;/p&gt;
&lt;h2 id="the-developer-case-for-apple-silicon"&gt;The Developer Case for Apple Silicon
&lt;/h2&gt;&lt;p&gt;This is where the decision got easy. And where most &amp;ldquo;switch to Mac&amp;rdquo; posts get lazy. They say &amp;ldquo;it&amp;rsquo;s Unix!&amp;rdquo; and move on. Let me spell out what that means and where it bites you.&lt;/p&gt;
&lt;p&gt;macOS is Unix-based. That changes the day-to-day development experience in concrete ways. But there are real trade-offs worth knowing before you commit.&lt;/p&gt;
&lt;h3 id="what-works-better"&gt;What Works Better
&lt;/h3&gt;&lt;p&gt;Out of the box you get zsh (or bash if you prefer), SSH, and a proper POSIX environment. No WSL layer. No PowerShell vs. bash context switching. No &amp;ldquo;which terminal am I in right now&amp;rdquo; mental overhead.&lt;/p&gt;
&lt;p&gt;When I SSH into my homelab servers, it feels identical to working locally. The key bindings are the same. The shell is the same. The config file syntax is the same. If you&amp;rsquo;re the kind of person who debates Debian vs. Ubuntu for servers or tinkers with pfSense vs. OPNsense configs, macOS fits naturally into that workflow. The tooling and conventions align with what you&amp;rsquo;re managing remotely.&lt;/p&gt;
&lt;h4 id="installing-homebrew"&gt;Installing Homebrew
&lt;/h4&gt;&lt;p&gt;If you&amp;rsquo;re coming from Windows, macOS doesn&amp;rsquo;t ship with a built-in package manager like &lt;code&gt;apt&lt;/code&gt; or &lt;code&gt;dnf&lt;/code&gt;. Homebrew fills that gap. It&amp;rsquo;s the de facto standard for installing developer tools on macOS, and you&amp;rsquo;ll want it set up before you do anything else.&lt;/p&gt;
&lt;p&gt;Open Terminal (it&amp;rsquo;s in Applications &amp;gt; Utilities, or hit &lt;code&gt;Cmd+Space&lt;/code&gt; and type &amp;ldquo;Terminal&amp;rdquo;) and run this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bin/bash -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The script will explain what it&amp;rsquo;s going to do and ask for your password. Read what it tells you. After it finishes, it prints instructions to add Homebrew to your shell path. On Apple Silicon Macs, Homebrew installs to &lt;code&gt;/opt/homebrew&lt;/code&gt; instead of the old &lt;code&gt;/usr/local&lt;/code&gt; path. The installer will show you the exact lines to paste. Follow them.&lt;/p&gt;
&lt;p&gt;Verify it&amp;rsquo;s working:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew --version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that prints a version number, you&amp;rsquo;re good. If it says &amp;ldquo;command not found,&amp;rdquo; you skipped the path step above. Go back and do it.&lt;/p&gt;
&lt;p&gt;Once Homebrew is set up, installing tools is one command each:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install --cask docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The difference between &lt;code&gt;brew install&lt;/code&gt; and &lt;code&gt;brew install --cask&lt;/code&gt; matters. &lt;code&gt;brew install&lt;/code&gt; is for command-line tools and libraries. &lt;code&gt;brew install --cask&lt;/code&gt; is for GUI applications (things with a window, like Docker Desktop, VS Code, or Firefox). Use the wrong one and you&amp;rsquo;ll get the wrong thing, or nothing at all.&lt;/p&gt;
&lt;p&gt;VS Code, Git, Docker Desktop, and most developer tools run natively on Apple Silicon. For standard workflows, there&amp;rsquo;s no emulation layer involved.&lt;/p&gt;
&lt;h3 id="the-arm64-vs-amd64-reality"&gt;The arm64 vs amd64 Reality
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s what the enthusiast posts skip over: Apple Silicon is arm64, and not every Docker image has an arm64 build.&lt;/p&gt;
&lt;p&gt;Most popular images do: postgres, redis, nginx, node, python, the whole *arr stack. But some older or niche images are x86-only. When you pull one of those on Apple Silicon, Docker runs it via Rosetta 2 emulation. For most cases this works fine, but it adds overhead and occasionally fails on images that rely on specific x86 instructions.&lt;/p&gt;
&lt;p&gt;You can check whether an image has arm64 support before pulling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker manifest inspect &amp;lt;image&amp;gt;:&amp;lt;tag&amp;gt; &lt;span class="p"&gt;|&lt;/span&gt; grep architecture
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see &lt;code&gt;&amp;quot;architecture&amp;quot;: &amp;quot;arm64&amp;quot;&lt;/code&gt;, you&amp;rsquo;re golden. If it&amp;rsquo;s only &lt;code&gt;amd64&lt;/code&gt;, you&amp;rsquo;re running under emulation.&lt;/p&gt;
&lt;p&gt;For day-to-day homelab and school dev work, I&amp;rsquo;ve hit this maybe twice in several months. It&amp;rsquo;s not a daily problem. But it&amp;rsquo;s real, and you should know it exists before you&amp;rsquo;re blocked on a deadline waiting on a container that refuses to start.&lt;/p&gt;
&lt;!-- REVIEW: Verify current Docker Desktop Rosetta emulation behavior - it has improved with each major release --&gt;
&lt;h3 id="rosetta-2-and-x86-only-tooling"&gt;Rosetta 2 and x86-Only Tooling
&lt;/h3&gt;&lt;p&gt;Rosetta 2 transparently runs x86 apps on Apple Silicon. For most apps and CLI tools, you won&amp;rsquo;t notice it&amp;rsquo;s happening. Some x86-only tools work fine under Rosetta. Others, particularly anything touching low-level kernel interfaces or hypervisor APIs, may not.&lt;/p&gt;
&lt;p&gt;You can verify whether a binary is running under Rosetta:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;file /path/to/binary
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Look for &amp;#34;arm64&amp;#34; (native) vs &amp;#34;x86_64&amp;#34; (Rosetta)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For typical school and homelab development, Rosetta handles the edge cases you&amp;rsquo;ll encounter. If you&amp;rsquo;re doing kernel development, low-level fuzzing, or complex hypervisor work, research your specific toolchain before switching platforms.&lt;/p&gt;
&lt;h3 id="vm-limitations"&gt;VM Limitations
&lt;/h3&gt;&lt;p&gt;Parallels Desktop runs Windows 11 on Arm well. Standard Linux VMs work fine. What&amp;rsquo;s limited is nested virtualization and complex multi-VM network labs. Some scenarios that work cleanly in VMware Workstation on x86 require workarounds (or don&amp;rsquo;t work at all) on Apple Silicon.&lt;/p&gt;
&lt;p&gt;For standard homelab test VMs and dev environments, Parallels is solid. For complex multi-VM network simulation (think: GNS3 labs with multiple routers and switches), a dedicated x86 box is still the better tool. Your Proxmox server in the closet is perfect for that.&lt;/p&gt;
&lt;h2 id="ram-and-storage-get-this-right-the-first-time"&gt;RAM and Storage: Get This Right the First Time
&lt;/h2&gt;&lt;p&gt;You cannot upgrade either after purchase. Apple solders everything to the board. Buy what you actually need, because there&amp;rsquo;s no fixing this later.&lt;/p&gt;
&lt;h3 id="ram"&gt;RAM
&lt;/h3&gt;&lt;p&gt;The M5 Air goes up to 32 GB of unified memory.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browsing and writing: 16 GB can work, but you&amp;rsquo;ll feel it within a year.&lt;/li&gt;
&lt;li&gt;Docker plus light VMs: 24 GB is the floor. Do not go below this for dev work.&lt;/li&gt;
&lt;li&gt;Parallels with Windows plus heavy multitasking: 32 GB. Don&amp;rsquo;t argue with me on this one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Parallels recommends allocating 8 to 12 GB of RAM and about 4 CPU cores for smooth Windows 11 on Arm. On 16 GB total, you&amp;rsquo;ll feel that constraint immediately. macOS itself wants 4 to 6 GB before you&amp;rsquo;ve opened anything.&lt;/p&gt;
&lt;p&gt;One thing worth knowing: Apple Silicon uses the same memory pool for CPU and GPU (unified memory architecture). Under heavy memory pressure, macOS compresses memory and swaps to the SSD rather than crashing. The SSD speeds make this much less painful than traditional disk swap, but it still shows up as latency spikes under sustained pressure. You can monitor this in Activity Monitor under the &amp;ldquo;Memory&amp;rdquo; tab. If that &amp;ldquo;Memory Pressure&amp;rdquo; gauge is regularly yellow or red, you bought the wrong RAM tier.&lt;/p&gt;
&lt;p&gt;I chose 32 GB. No regrets.&lt;/p&gt;
&lt;h3 id="storage"&gt;Storage
&lt;/h3&gt;&lt;p&gt;Base is 512 GB. That sounds like a lot until you start filling it.&lt;/p&gt;
&lt;p&gt;A Windows 11 Arm VM consumes 30 to 80 GB. Add projects, Docker images, photos, and blog assets and 512 GB disappears fast. Docker images alone can eat 20 to 50 GB if you&amp;rsquo;re not pruning regularly (&lt;code&gt;docker system prune&lt;/code&gt; is your friend).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 TB minimum if you&amp;rsquo;re doing dev plus VM work.&lt;/li&gt;
&lt;li&gt;2 TB if this is your only machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Neither is upgradeable. Buy once, buy right.&lt;/p&gt;
&lt;h2 id="where-macos-hurts"&gt;Where macOS Hurts
&lt;/h2&gt;&lt;p&gt;No platform is perfect. Here&amp;rsquo;s where macOS will make you swear.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software licensing and Windows-only tools.&lt;/strong&gt; Some enterprise software, niche tools, and specialized applications are Windows-only with no viable Mac alternative. If your school or employer requires specific Windows software, check compatibility before you buy. Windows on Arm via Parallels handles most productivity apps, but not everything. Test before you commit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Corporate IT environments.&lt;/strong&gt; If you&amp;rsquo;re in a managed Windows environment where IT controls your machine, a personal Mac doesn&amp;rsquo;t change your work situation. This post assumes you&amp;rsquo;re buying your own hardware for personal use and school.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Peripheral compatibility.&lt;/strong&gt; Most peripherals work fine. Some older USB dongles, specialized input devices, and certain external audio interfaces have better Windows driver support. I haven&amp;rsquo;t hit a real problem, but check anything specialized before purchasing. Particularly older printers and scanners.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The adjustment period is real.&lt;/strong&gt; Finder and macOS window management are genuinely different from Windows. Not worse. Different. Rectangle (free, takes five minutes to install) fixes the window snapping problem that will drive you crazy on day one. The broader adjustment takes a couple of weeks. Some long-term Windows users take a month or two to fully re-wire their muscle memory.&lt;/p&gt;
&lt;h2 id="the-ecosystem-adjustment"&gt;The Ecosystem Adjustment
&lt;/h2&gt;&lt;p&gt;Keyboard shortcuts shift. &lt;code&gt;Ctrl&lt;/code&gt; becomes &lt;code&gt;Cmd&lt;/code&gt; for most things. Copy is &lt;code&gt;Cmd+C&lt;/code&gt;, terminal interrupt stays &lt;code&gt;Ctrl+C&lt;/code&gt;. It takes about a week to stop hitting the wrong key.&lt;/p&gt;
&lt;p&gt;What surprised me after the adjustment: how consistent everything feels, how rarely the system stutters, and how predictable updates are.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s boring. That&amp;rsquo;s exactly what you want from a work machine. Boring is reliable. Boring means you&amp;rsquo;re thinking about your code, not your OS.&lt;/p&gt;
&lt;h2 id="one-cable-at-your-desk"&gt;One Cable at Your Desk
&lt;/h2&gt;&lt;p&gt;At first, two Thunderbolt ports felt limiting. I came from a desktop with more USB ports than I could fill.&lt;/p&gt;
&lt;p&gt;One good Thunderbolt dock fixed that. One cable now handles power, dual monitors, Ethernet, keyboard, mouse, and external storage. I use a CalDigit TS4, but there are solid options from OWC and Anker too.&lt;/p&gt;
&lt;p&gt;Unplug that one cable and you&amp;rsquo;re mobile. Plug it back in and you&amp;rsquo;re docked. That&amp;rsquo;s actually cleaner than my old Windows docking setup.&lt;/p&gt;
&lt;h2 id="my-windows-desktop-has-a-new-job"&gt;My Windows Desktop Has a New Job
&lt;/h2&gt;&lt;p&gt;I didn&amp;rsquo;t abandon Windows. I redefined what it&amp;rsquo;s for.&lt;/p&gt;
&lt;p&gt;The desktop now boots into Steam, runs games, and handles GPU-heavy workloads. That&amp;rsquo;s its whole job. It does that job extremely well.&lt;/p&gt;
&lt;p&gt;Windows still does gaming better than any other platform. Native support, mature GPU drivers, the whole PC gaming ecosystem is built around it. No argument there.&lt;/p&gt;
&lt;p&gt;Trying to make one machine handle everything created constant friction. Letting each machine do what it&amp;rsquo;s good at removed it.&lt;/p&gt;
&lt;p&gt;The Mac handles school, dev, writing, and travel. The PC handles games. Everybody wins.&lt;/p&gt;
&lt;h2 id="who-should-pick-which"&gt;Who Should Pick Which
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Get a Windows laptop if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You game on the go.&lt;/li&gt;
&lt;li&gt;You rely on Windows-only software with no viable alternative.&lt;/li&gt;
&lt;li&gt;Your school or employer requires specific Windows tooling.&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re on a tight budget where the Mac premium doesn&amp;rsquo;t fit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Get the M5 MacBook Air if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re a developer managing servers or a homelab.&lt;/li&gt;
&lt;li&gt;You travel frequently and care about weight and battery.&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re tired of Windows sluggishness and forced reboots.&lt;/li&gt;
&lt;li&gt;You want a silent, reliable machine that gets out of your way.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Non-negotiables if you go Mac:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;24 GB RAM minimum for dev work with Docker.&lt;/li&gt;
&lt;li&gt;32 GB if you plan to run Parallels regularly.&lt;/li&gt;
&lt;li&gt;1 TB storage minimum if you&amp;rsquo;ll use VMs.&lt;/li&gt;
&lt;li&gt;Budget for a good Thunderbolt dock ($150 to $400 depending on features).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The learning curve is real. It&amp;rsquo;s shorter than you&amp;rsquo;d expect.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;h3 id="does-the-m5-macbook-air-handle-ssh-to-a-homelab-well"&gt;Does the M5 MacBook Air handle SSH to a homelab well?
&lt;/h3&gt;&lt;p&gt;Yes. macOS provides native SSH and a full terminal environment out of the box. No extra software needed. For remote server management and development workflows, it performs great. If you manage pfSense, OPNsense, Proxmox, or Linux servers in your homelab, the tooling feels natural from macOS. You can also use &lt;code&gt;~/.ssh/config&lt;/code&gt; to set up aliases for your homelab hosts, exactly like you would on any Linux box.&lt;/p&gt;
&lt;h3 id="what-about-docker-on-apple-silicon"&gt;What about Docker on Apple Silicon?
&lt;/h3&gt;&lt;p&gt;Docker Desktop runs natively on Apple Silicon. Most popular images have arm64 builds. Some older or niche images are x86-only and run via Rosetta 2 emulation, which works for most cases but can add overhead. Check image compatibility with &lt;code&gt;docker manifest inspect&lt;/code&gt; before building your stack around it. For typical homelab and school projects, it&amp;rsquo;s rarely a blocking problem.&lt;/p&gt;
&lt;h3 id="how-much-ram-do-i-need-if-im-running-parallels"&gt;How much RAM do I need if I&amp;rsquo;m running Parallels?
&lt;/h3&gt;&lt;p&gt;Parallels recommends 8 to 12 GB and about 4 CPU cores for Windows 11 on Arm. That means 24 to 32 GB total for comfortable multitasking alongside your other apps. On 16 GB, you&amp;rsquo;ll feel the squeeze fast.&lt;/p&gt;
&lt;h3 id="will-the-fanless-design-throttle-on-long-builds"&gt;Will the fanless design throttle on long builds?
&lt;/h3&gt;&lt;p&gt;For typical dev compiles and school multitasking, it performs well without noticeable throttling. Sustained heavy rendering or extreme build pipelines running all day are better served by a MacBook Pro with active cooling. The Air will thermal throttle under prolonged 100% CPU load. That&amp;rsquo;s physics, not a defect.&lt;/p&gt;
&lt;h3 id="whats-real-battery-life-like"&gt;What&amp;rsquo;s real battery life like?
&lt;/h3&gt;&lt;p&gt;Apple rates it at up to 18 hours. In mixed use with Wi-Fi, writing, and dev tasks, expect 10 to 14 hours. Heavy Docker builds and VM usage will pull that number down. Light browsing and writing will push it up.&lt;/p&gt;
&lt;h3 id="can-i-run-windows-on-the-mac"&gt;Can I run Windows on the Mac?
&lt;/h3&gt;&lt;p&gt;Yes, via Parallels Desktop. Windows 11 on Arm runs smoothly for Office and productivity apps. It is not a gaming replacement. Keep a Windows desktop for that.&lt;/p&gt;
&lt;h3 id="is-a-thunderbolt-dock-worth-it"&gt;Is a Thunderbolt dock worth it?
&lt;/h3&gt;&lt;p&gt;If you work at a desk, absolutely. A good Thunderbolt dock handles charging, dual displays, Ethernet, and peripherals through one cable. It makes the two-port limitation a non-issue. Budget $150 to $400 depending on how many monitors and ports you need.&lt;/p&gt;
&lt;h2 id="the-real-decision"&gt;The Real Decision
&lt;/h2&gt;&lt;p&gt;Switching to the M5 MacBook Air wasn&amp;rsquo;t about platform loyalty. It was about friction.&lt;/p&gt;
&lt;p&gt;My Windows machine slowly became something I had to manage around. Forced reboots, background sluggishness, and enterprise-gated features chipped away at the experience until I was spending mental energy on the OS instead of on the work.&lt;/p&gt;
&lt;p&gt;The M5 MacBook Air gave me silent operation, genuine all-day battery, Unix-native development tools, and predictable behavior. It gets out of the way and lets me work.&lt;/p&gt;
&lt;p&gt;Keeping the Windows desktop for gaming stopped me from forcing one machine to do everything. That&amp;rsquo;s a trap I see people fall into constantly.&lt;/p&gt;
&lt;p&gt;The real win isn&amp;rsquo;t macOS over Windows. It&amp;rsquo;s using the right tool for the job.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re heading back to school, doing development, managing a homelab, or traveling regularly, the M5 MacBook Air deserves a serious look.&lt;/p&gt;
&lt;p&gt;And if you&amp;rsquo;re still on the fence, ask yourself one thing: are you fighting your computer, or is it helping you?&lt;/p&gt;
&lt;p&gt;That answer usually makes the decision clear.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0FKRF82DX"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-dual-band-7_hu_ce304ea7fa95c4b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-dual-band-7_hu_6618450a0acf8e8e.webp" width="600" height="602" alt="TP-Link Omada WiFi 7 AP (Dual-Band, 2.5GbE)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
A ceiling-mount WiFi 7 access point with a 2.5GbE PoE uplink, Omada controller support, and VLAN-aware SSIDs. Dual-band (2.4/5 GHz) with MLO-capable radios for lower latency. The mid-tier pick for homelab and home networks that want WiFi 7 throughput without the tri-band premium.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47x7SCk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>WireGuard Client on OPNsense: VPS Tunnel LAN Routing Guide</title><link>https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/</link><pubDate>Fri, 06 Mar 2026 07:12:23 +0000</pubDate><guid>https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/featured_hu_db5f0754f6ddb59c.webp" alt="Featured image of post WireGuard Client on OPNsense: VPS Tunnel LAN Routing Guide" /&gt;&lt;p&gt;If you&amp;rsquo;re running services at home and want to expose them through a VPS reverse proxy, configuring a WireGuard client on OPNsense is one of the cleanest ways to do it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the setup: a VPS running Caddy (or nginx, or whatever reverse proxy you prefer) accepts requests from the internet and forwards them through a WireGuard tunnel to services on your LAN. Jellyfin, Home Assistant, Nextcloud, whatever you&amp;rsquo;re running. Traffic enters through the tunnel and leaves the same way. Your home IP stays hidden. No ports open on your home router.&lt;/p&gt;
&lt;p&gt;Most guides make this harder than it needs to be. They throw policy routing, full-tunnel configurations, and asymmetric routing fixes at you before you&amp;rsquo;ve even gotten a handshake. You don&amp;rsquo;t need any of that for a reverse proxy setup. The VPS initiates the connection, your LAN service replies, and the reply naturally goes back through the tunnel because that&amp;rsquo;s where it came from. Routing 101.&lt;/p&gt;
&lt;p&gt;The only real work is on OPNsense: get the tunnel up, assign the interface, and write a tight firewall rule that lets the VPS reach your service and nothing else.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The pfSense details might be a little off as I haven&amp;rsquo;t used pfSense in a few years.&lt;/em&gt;&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
&lt;p&gt;Set up a WireGuard client instance in OPNsense. Assign it as an interface. Add a firewall rule that allows VPS traffic to reach your specific service (like Jellyfin) and nothing else. On the VPS, add a route for your LAN subnet through the tunnel and point Caddy at your service&amp;rsquo;s LAN IP.&lt;/p&gt;
&lt;p&gt;No policy routing. No outbound NAT. No asymmetric routing fixes. This is a reverse proxy setup, not a full tunnel.&lt;/p&gt;
&lt;p&gt;pfSense can do the same with its WireGuard package, but OPNsense&amp;rsquo;s integration is cleaner.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;How the Traffic Flows
Before touching any config, understand what&amp;rsquo;s actually happening. If you skip this section and something breaks later, you&amp;rsquo;ll be guessing. Don&amp;rsquo;t guess.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Internet client sends a request to &lt;code&gt;https://jellyfin.yourdomain.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DNS resolves to your VPS public IP&lt;/li&gt;
&lt;li&gt;Caddy on the VPS terminates TLS and reverse proxies the request through the WireGuard tunnel to your Jellyfin server&amp;rsquo;s LAN IP (e.g., 192.168.0.50:8096)&lt;/li&gt;
&lt;li&gt;OPNsense receives the packet on the WireGuard interface &lt;code&gt;wg0&lt;/code&gt;, checks firewall rules, and routes it to your LAN&lt;/li&gt;
&lt;li&gt;Jellyfin processes the request and replies to the source IP &lt;code&gt;10.10.10.1&lt;/code&gt;, the VPS tunnel IP&lt;/li&gt;
&lt;li&gt;OPNsense sees the destination is in the tunnel subnet &lt;code&gt;10.10.10.0/24&lt;/code&gt; and sends it back out &lt;code&gt;wg0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VPS receives the reply, Caddy sends the response to the internet client&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key thing: replies go back through the tunnel naturally because the VPS tunnel IP &lt;code&gt;10.10.10.1&lt;/code&gt; is in the tunnel subnet. OPNsense already knows how to reach it. No special routing needed.&lt;/p&gt;
&lt;p&gt;This is why a reverse proxy setup is simpler than routing all your LAN traffic through a VPS. Traffic that enters through the tunnel leaves through the tunnel. No asymmetric routing, no state mismatches, no dropped packets, no Reddit posts asking why nothing works.&lt;/p&gt;
&lt;p&gt;Prerequisites
Before touching OPNsense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS running WireGuard server (already configured and tested)&lt;/li&gt;
&lt;li&gt;VPS public IP and WireGuard port, default 51820/UDP&lt;/li&gt;
&lt;li&gt;VPS WireGuard public key&lt;/li&gt;
&lt;li&gt;Tunnel IPs chosen (I use &lt;code&gt;10.10.10.1&lt;/code&gt; for the VPS, &lt;code&gt;10.10.10.2&lt;/code&gt; for OPNsense)&lt;/li&gt;
&lt;li&gt;Reverse proxy installed on VPS (Caddy, nginx, etc.)&lt;/li&gt;
&lt;li&gt;VPS WireGuard [Peer] block has your OPNsense public key, and its AllowedIPs includes your LAN subnet (e.g., 192.168.0.0/24) so the VPS knows to route LAN-bound traffic through the tunnel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Recommended:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OPNsense 24.x or newer&lt;/li&gt;
&lt;li&gt;Config backup before changes. &lt;strong&gt;System &amp;gt; Configuration &amp;gt; Backups&lt;/strong&gt;, download the XML. Do it now. Not after you&amp;rsquo;ve broken something. Now.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On OPNsense 25.x, WireGuard is built in. On 24.x and earlier, it&amp;rsquo;s a plugin.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re on 24.x:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;System &amp;gt; Firmware &amp;gt; Plugins&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;os-wireguard&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then regardless of version:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; General&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Enable WireGuard&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;&lt;br&gt;
A fanless N150 appliance with six Intel i226-V 2.5GbE ports and hardware AES-NI, built for OPNsense or pfSense. Six ports give you room to separate WAN, LAN, and a couple of VLANs without adding a switch, which keeps the routing rules in this guide easier to follow.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-2-create-the-wireguard-client-instance"&gt;Step 2: Create the WireGuard Client Instance
&lt;/h2&gt;&lt;p&gt;Go to:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; Instances &amp;gt; Add&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Configure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enabled: Yes&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;VPS-WG&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Generate a new key pair using the &lt;code&gt;Gear icon&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tunnel Address: &lt;code&gt;10.10.10.2/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;MTU: &lt;code&gt;1420&lt;/code&gt; for bare metal, &lt;code&gt;1380&lt;/code&gt; if OPNsense is virtualized (QEMU/KVM), &lt;code&gt;1412&lt;/code&gt; if your WAN uses PPPoE&lt;/li&gt;
&lt;li&gt;Disable Routes: &lt;strong&gt;Unchecked&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save.&lt;/p&gt;
&lt;p&gt;Now add a Peer:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; Peers &amp;gt; Add&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enabled: Yes&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;VPS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Public Key: your VPS server&amp;rsquo;s public key&lt;/li&gt;
&lt;li&gt;Tunnel Address (AllowedIPs): &lt;code&gt;10.10.10.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Endpoint Address: &lt;code&gt;Your VPS public IP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Endpoint Port: &lt;code&gt;51820&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Instances: &lt;code&gt;VPS-WG&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Persistent Keepalive: &lt;code&gt;25&lt;/code&gt; seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After saving the peer, go back to your &lt;code&gt;VPS-WG&lt;/code&gt; instance, and under &lt;strong&gt;Peers&lt;/strong&gt;, select the &lt;code&gt;VPS&lt;/code&gt; peer you created. Save and Apply.&lt;/p&gt;
&lt;h3 id="why-allowedips-is-101010024-and-not-00000"&gt;Why AllowedIPs is &lt;code&gt;10.10.10.0/24&lt;/code&gt; and Not &lt;code&gt;0.0.0.0/0&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;re not routing LAN internet traffic through the VPS. You only need OPNsense to know about the tunnel subnet. Traffic destined for &lt;code&gt;10.10.10.1&lt;/code&gt; goes through &lt;code&gt;wg0&lt;/code&gt;. Everything else goes out WAN as usual.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0.0.0.0/0&lt;/code&gt; is for full-tunnel setups where you want LAN devices to egress through the VPS. That&amp;rsquo;s a different guide with different problems: policy routing, outbound NAT, asymmetric routing. Don&amp;rsquo;t go there unless you mean to.&lt;/p&gt;
&lt;h2 id="step-3-assign-the-wireguard-interface"&gt;Step 3: Assign the WireGuard Interface
&lt;/h2&gt;&lt;p&gt;This is where many people stop too early. The tunnel works for OPNsense-to-VPS pings at this point. But without an interface assignment, you can&amp;rsquo;t write firewall rules for tunnel traffic. That means any traffic the VPS sends toward your LAN gets silently dropped. No log entry. No error. Only silence and confusion.&lt;/p&gt;
&lt;p&gt;Go to:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You should see &lt;code&gt;wg0&lt;/code&gt; available in the dropdown. Add it.&lt;/p&gt;
&lt;p&gt;Then click on the newly created interface (it&amp;rsquo;ll show as something like &lt;code&gt;OPT1&lt;/code&gt;) and configure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable: checked&lt;/li&gt;
&lt;li&gt;Description: &lt;code&gt;RackNerdVPS&lt;/code&gt; (or whatever name makes sense for your VPS provider)&lt;/li&gt;
&lt;li&gt;IPv4 Configuration Type: None&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save and apply.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &amp;ldquo;None&amp;rdquo; and not &amp;ldquo;Static&amp;rdquo;?&lt;/strong&gt; The tunnel address (&lt;code&gt;10.10.10.2/24&lt;/code&gt;) is already configured on the WireGuard instance itself. Setting Static here and re-entering the same address can cause conflicts or weird behavior. Let the instance own the IP. The interface assignment only gives OPNsense a handle to attach firewall rules to. That&amp;rsquo;s the only reason you&amp;rsquo;re assigning it.&lt;/p&gt;
&lt;h2 id="step-4-firewall-rule-for-vps-access-to-your-lan-service"&gt;Step 4: Firewall Rule for VPS Access to Your LAN Service
&lt;/h2&gt;&lt;p&gt;This is the most important step, and it&amp;rsquo;s where most people get lazy and create a wide-open rule. Don&amp;rsquo;t be that person. You will regret it exactly once, and that once will ruin a weekend.&lt;/p&gt;
&lt;p&gt;Go to:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; RackNerdVPS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add a rule:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enabled: Yes&lt;/li&gt;
&lt;li&gt;Description: &lt;code&gt;Allow VPS to Jellyfin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Action: Pass&lt;/li&gt;
&lt;li&gt;Direction: In&lt;/li&gt;
&lt;li&gt;Protocol: TCP&lt;/li&gt;
&lt;li&gt;Source: &lt;code&gt;RackNerdVPS net&lt;/code&gt; (or &lt;code&gt;10.10.10.0/24&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Destination: Single host, &lt;code&gt;192.168.0.50&lt;/code&gt; (your Jellyfin server&amp;rsquo;s IP)&lt;/li&gt;
&lt;li&gt;Destination Port: &lt;code&gt;8096&lt;/code&gt; (Jellyfin&amp;rsquo;s default port)&lt;/li&gt;
&lt;li&gt;Log: Enable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save and apply.&lt;/p&gt;
&lt;p&gt;This rule says: traffic arriving through the tunnel can reach Jellyfin and nothing else. If someone compromises your VPS, they can talk to Jellyfin on port 8096. They cannot reach your NAS, your Pi-hole, your Home Assistant, or anything else on your LAN. That&amp;rsquo;s a massive difference from a rule that passes &lt;code&gt;any&lt;/code&gt; to &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="adding-more-services-later"&gt;Adding More Services Later
&lt;/h3&gt;&lt;p&gt;If you add more services behind the VPS (Home Assistant, Nextcloud, etc.), don&amp;rsquo;t widen the existing rule. Add separate rules for each service with specific destination IPs and ports. Or create an alias:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Firewall &amp;gt; Aliases &amp;gt; Add&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;VPS_Allowed_Hosts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Type: Host(s)&lt;/li&gt;
&lt;li&gt;Content: &lt;code&gt;192.168.0.50&lt;/code&gt;, &lt;code&gt;192.168.0.51&lt;/code&gt; (add each service host IP)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then use the alias as the destination in a single rule. Either way, keep it explicit. Blanket allow rules on a tunnel interface are how you wake up one morning with someone else browsing your NAS.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D461YN33"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/SG3210X-M2_hu_933dcc0d5cd85f33.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/SG3210X-M2_hu_d7bfa040668d5558.webp" width="600" height="150" alt="TP-Link Omada SG3210X-M2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada SG3210X-M2&lt;/strong&gt;&lt;br&gt;
Full-featured, compact, rack-ready. Eight multi-gig ports, dual 10GbE uplinks, VLAN/QoS/ACL/LACP, and seamless integration with TP-Link&amp;rsquo;s Omada controller. It locks down your Jellyfin/NAS traffic while scaling effortlessly with your homelab.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/44ZP0Jk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-5-vps-side-configuration"&gt;Step 5: VPS-Side Configuration
&lt;/h2&gt;&lt;p&gt;The OPNsense side is done. Now the VPS needs to know how to reach your LAN through the tunnel.&lt;/p&gt;
&lt;h3 id="route-for-your-lan-subnet"&gt;Route for Your LAN Subnet
&lt;/h3&gt;&lt;p&gt;The VPS needs a static route so it knows that &lt;code&gt;192.168.0.0/24&lt;/code&gt; is reachable through the tunnel. Without it, the VPS has no idea your LAN exists on the other side of &lt;code&gt;wg0&lt;/code&gt;. The cleanest way is to add it to your WireGuard config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/wireguard/wg0.conf on the VPS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.1/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ListenPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;VPS_PRIVATE_KEY&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PublicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;OPNSENSE_PUBLIC_KEY&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AllowedIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.2/32, 192.168.0.0/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The important line is &lt;code&gt;AllowedIPs&lt;/code&gt; on the peer. It includes &lt;code&gt;192.168.0.0/24&lt;/code&gt;. Without this, WireGuard on the VPS will refuse to send LAN-bound packets through the tunnel. It won&amp;rsquo;t know that subnet belongs to this peer.&lt;/p&gt;
&lt;p&gt;After editing, restart WireGuard:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart wg-quick@wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify the route exists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip route &lt;span class="p"&gt;|&lt;/span&gt; grep 192.168
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see: &lt;code&gt;192.168.0.0/24 dev wg0 scope link&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t see it, WireGuard didn&amp;rsquo;t come up cleanly. Check &lt;code&gt;sudo wg show&lt;/code&gt; and &lt;code&gt;journalctl -u wg-quick@wg0&lt;/code&gt; for errors.&lt;/p&gt;
&lt;p&gt;Working &lt;code&gt;wg show&lt;/code&gt; example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;interface: wg0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; public key: &amp;lt;public key&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; private key: &lt;span class="o"&gt;(&lt;/span&gt;hidden&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; listening port: &lt;span class="m"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;peer: &amp;lt;public key&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; endpoint: &amp;lt;redacted&amp;gt;:5853
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allowed ips: 10.10.10.2/32, 192.168.0.0/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; latest handshake: &lt;span class="m"&gt;1&lt;/span&gt; minute, &lt;span class="m"&gt;28&lt;/span&gt; seconds ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; transfer: 42.86 GiB received, 950.45 MiB sent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="caddy-configuration"&gt;Caddy Configuration
&lt;/h3&gt;&lt;p&gt;Point Caddy at your Jellyfin server&amp;rsquo;s LAN IP through the tunnel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddy" data-lang="caddy"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;jellyfin.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;log&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt; &lt;span class="s"&gt;/var/log/caddy/jellyfin.access.log&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;roll_size&lt;/span&gt; &lt;span class="s"&gt;100mb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;roll_keep&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;format&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;request_body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;max_size&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;LAN&lt;/span&gt; &lt;span class="s"&gt;IP&amp;gt;:8096&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="se"&gt;{host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{remote_host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;flush_interval&lt;/span&gt; &lt;span class="mi"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transport&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;read_buffer&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="c1"&gt; # 64KB - reasonable size
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;write_buffer&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="c1"&gt; # 64KB
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;dial_timeout&lt;/span&gt; &lt;span class="mi"&gt;30s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;response_header_timeout&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Caddy sends the request to &lt;code&gt;LAN IP&lt;/code&gt;. The VPS route table says &lt;code&gt;192.168.0.0/24&lt;/code&gt; goes via &lt;code&gt;10.10.10.2&lt;/code&gt; through &lt;code&gt;wg0&lt;/code&gt;. The packet enters the tunnel, OPNsense receives it, the firewall rule allows it, and Jellyfin gets the request.&lt;/p&gt;
&lt;h3 id="test-end-to-end"&gt;Test End-to-End
&lt;/h3&gt;&lt;p&gt;From the VPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Can you reach OPNsense through the tunnel?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping 10.10.10.2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Can you reach Jellyfin through the tunnel?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://&amp;lt;LAN IP&amp;gt;:8096
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the ping works but the curl doesn&amp;rsquo;t, the problem is almost certainly your OPNsense firewall rule. Check the firewall logs under &lt;strong&gt;Firewall &amp;gt; Log Files &amp;gt; Live View&lt;/strong&gt; and filter on the &lt;code&gt;RackNerdVPS&lt;/code&gt; interface. Look for blocked entries with the destination &lt;code&gt;Jellyfin's LAN IP&lt;/code&gt;. If you see blocks, your rule is either missing, disabled, or the destination IP/port doesn&amp;rsquo;t match what Caddy is actually sending.&lt;/p&gt;
&lt;p&gt;If both fail, go back to &amp;ldquo;Verify the Handshake&amp;rdquo; and start there.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;&lt;br&gt;
A compact mini-workstation built around up to a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. Great as a homelab node or small server with serious I/O.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="mtu-tuning"&gt;MTU Tuning
&lt;/h2&gt;&lt;p&gt;If large requests stall (pages half-load, big video files buffer forever, SSH works but SCP hangs), your MTU is too high. This is the single most common &amp;ldquo;it works but not really&amp;rdquo; problem with WireGuard tunnels. Every time. Without fail. Someone in a forum will tell you it&amp;rsquo;s DNS. It&amp;rsquo;s not DNS. It&amp;rsquo;s your MTU.&lt;/p&gt;
&lt;p&gt;WireGuard adds 60 bytes of overhead. Start with:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Starting MTU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bare metal, standard Ethernet WAN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1420&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PPPoE WAN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1412&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtualized OPNsense (QEMU/KVM/Proxmox)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1380&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PPPoE + VLAN tagging&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1400&lt;/code&gt; or lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Test with a do-not-fragment ping from OPNsense to the VPS tunnel IP:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ping -s 1352 -M do 10.10.10.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; value should be your MTU minus 28 (that&amp;rsquo;s 20 bytes of IP header plus 8 bytes of ICMP header). If it fragments or times out, lower the MTU in your WireGuard instance settings (&lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; Instances&lt;/strong&gt;, edit your instance) and test again. Drop by 20 each time until the ping goes through clean.&lt;/p&gt;
&lt;p&gt;For a reverse proxy setup, MTU issues usually show up as Jellyfin streams that start playing and then stall, or web UIs that load the page skeleton but never finish rendering. Small requests work fine. Large ones die. That&amp;rsquo;s your MTU.&lt;/p&gt;
&lt;h2 id="pfsense-wireguard-setup-step-by-step"&gt;pfSense WireGuard Setup (Step-by-Step)
&lt;/h2&gt;&lt;p&gt;The concepts are identical, but the UI and some behaviors differ.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;System &amp;gt; Package Manager &amp;gt; Available Packages&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Install the WireGuard package&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike OPNsense 25.x, pfSense doesn&amp;rsquo;t ship WireGuard integrated. It&amp;rsquo;s package-based. Recent versions improved stability significantly, but OPNsense still feels more cohesive for WireGuard setups.&lt;/p&gt;
&lt;h3 id="pfsense-client-configuration"&gt;pfSense Client Configuration
&lt;/h3&gt;&lt;p&gt;Go to:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; Tunnels &amp;gt; Add&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable: Yes&lt;/li&gt;
&lt;li&gt;Description: &lt;code&gt;VPS-WG&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Listen Port: leave blank (client mode)&lt;/li&gt;
&lt;li&gt;Interface Keys: generate or paste your private key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save, then go to &lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; Peers &amp;gt; Add&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tunnel: select &lt;code&gt;VPS-WG&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Public Key: VPS public key&lt;/li&gt;
&lt;li&gt;Endpoint: &lt;code&gt;VPS_PUBLIC_IP:51820&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Allowed IPs: &lt;code&gt;10.10.10.0/24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Persistent Keepalive: &lt;code&gt;25&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save.&lt;/p&gt;
&lt;h3 id="assign-the-interface-in-pfsense"&gt;Assign the Interface in pfSense
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Interfaces &amp;gt; Assignments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The WireGuard tunnel appears as &lt;code&gt;tun_wgX&lt;/code&gt;. Add it, enable it, rename to &lt;code&gt;RackNerdVPS&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Set IPv4 to Static, address &lt;code&gt;10.10.10.2/24&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the opposite of OPNsense.&lt;/strong&gt; pfSense wants you to set the static IP on the interface assignment. OPNsense has the instance own the IP and the interface assignment uses &amp;ldquo;None.&amp;rdquo; Get this backwards on either platform and you&amp;rsquo;ll have a confusing afternoon.&lt;/p&gt;
&lt;h3 id="firewall-rule-in-pfsense"&gt;Firewall Rule in pfSense
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Firewall &amp;gt; Rules &amp;gt; RackNerdVPS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Same logic as OPNsense. Create a rule allowing traffic from the WG net to your Jellyfin host on its specific port. Don&amp;rsquo;t pass everything.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Action: Pass&lt;/li&gt;
&lt;li&gt;Protocol: TCP&lt;/li&gt;
&lt;li&gt;Source: &lt;code&gt;RackNerdVPS net&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Destination: &lt;code&gt;Jellyfin's LAN IP&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Destination Port: &lt;code&gt;8096&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;&lt;br&gt;
Plug-and-play 2.5GbE PCIe card that unlocks multi-gig speeds for about $30. Works out of the box with Proxmox, Linux, and Windows. No drama, faster transfers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="pfsense-vs-opnsense-for-wireguard"&gt;pfSense vs OPNsense for WireGuard
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;OPNsense&lt;/th&gt;
&lt;th&gt;pfSense&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard integration&lt;/td&gt;
&lt;td&gt;Built-in (25.x) / Plugin (24.x)&lt;/td&gt;
&lt;td&gt;Package (add-on)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config model&lt;/td&gt;
&lt;td&gt;Instances + Peers&lt;/td&gt;
&lt;td&gt;Tunnels + Peers tabs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stability (2024+)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Much improved, still package-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway monitoring&lt;/td&gt;
&lt;td&gt;Straightforward&lt;/td&gt;
&lt;td&gt;Requires more configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI clarity&lt;/td&gt;
&lt;td&gt;More intuitive&lt;/td&gt;
&lt;td&gt;Functional but more legacy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For WireGuard-heavy homelab setups, I&amp;rsquo;d lean OPNsense. But pfSense works. Both get the job done once configured.&lt;/p&gt;
&lt;h2 id="troubleshooting-guide"&gt;Troubleshooting Guide
&lt;/h2&gt;&lt;h3 id="no-handshake"&gt;No Handshake
&lt;/h3&gt;&lt;p&gt;Check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS firewall allows UDP on the WireGuard port&lt;/li&gt;
&lt;li&gt;Endpoint IP and port are correct&lt;/li&gt;
&lt;li&gt;Keys match (double-check copy-paste errors, especially trailing whitespace)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;wg show&lt;/code&gt; on both OPNsense and VPS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If behind NAT, ensure Persistent Keepalive is set to 25. Without it, the NAT mapping times out and the VPS can no longer reach your OPNsense.&lt;/p&gt;
&lt;h3 id="tunnel-is-up-but-vps-cant-reach-jellyfin"&gt;Tunnel is Up but VPS Can&amp;rsquo;t Reach Jellyfin
&lt;/h3&gt;&lt;p&gt;The handshake works, &lt;code&gt;ping 10.10.10.2&lt;/code&gt; from the VPS works, but &lt;code&gt;curl http://&amp;lt;Jellyfin's LAN IP&amp;gt;:8096&lt;/code&gt; fails. Work through these in order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No route on the VPS.&lt;/strong&gt; Run &lt;code&gt;ip route | grep 192.168&lt;/code&gt; on the VPS. If there&amp;rsquo;s no route, add one. The VPS doesn&amp;rsquo;t magically know your LAN subnet exists behind the tunnel.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Quick fix (doesn&amp;#39;t survive reboot):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ip route add 192.168.0.0/24 via 10.10.10.2 dev wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;AllowedIPs on the VPS peer doesn&amp;rsquo;t include your LAN subnet.&lt;/strong&gt; WireGuard won&amp;rsquo;t send packets to &lt;code&gt;192.168.0.0/24&lt;/code&gt; through the tunnel unless that subnet is in the peer&amp;rsquo;s &lt;code&gt;AllowedIPs&lt;/code&gt;. Check &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on the VPS and make sure the &lt;code&gt;[Peer]&lt;/code&gt; block includes &lt;code&gt;192.168.0.0/24&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense firewall is blocking it.&lt;/strong&gt; Check &lt;strong&gt;Firewall &amp;gt; Log Files &amp;gt; Live View&lt;/strong&gt; and filter on the &lt;code&gt;RackNerdVPS&lt;/code&gt; interface. If you see blocks, your firewall rule is either missing, misconfigured, or the destination IP/port doesn&amp;rsquo;t match what Caddy is sending.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jellyfin is only listening on localhost.&lt;/strong&gt; Some services bind to &lt;code&gt;127.0.0.1&lt;/code&gt; by default. Check Jellyfin&amp;rsquo;s network settings (Dashboard &amp;gt; Networking) and make sure it&amp;rsquo;s listening on &lt;code&gt;0.0.0.0&lt;/code&gt; or its LAN IP. If it&amp;rsquo;s bound to localhost, it will ignore everything that isn&amp;rsquo;t coming from the same machine.&lt;/p&gt;
&lt;h3 id="jellyfin-loads-but-streams-buffer-or-stall"&gt;Jellyfin Loads but Streams Buffer or Stall
&lt;/h3&gt;&lt;p&gt;Small requests work (the web UI loads) but video playback stalls. This is almost always an MTU problem. Large packets are getting fragmented or dropped.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lower the MTU on your WireGuard instance (try &lt;code&gt;1380&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Test with a do-not-fragment ping (see MTU Tuning section above)&lt;/li&gt;
&lt;li&gt;If you&amp;rsquo;re on a virtualized OPNsense, the hypervisor network stack often shaves off extra bytes. Go lower.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="no-such-device-wg0"&gt;&amp;ldquo;no such device wg0&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;This means the WireGuard interface doesn&amp;rsquo;t exist yet. Either the instance isn&amp;rsquo;t enabled in WireGuard settings, or the WireGuard service isn&amp;rsquo;t started.&lt;/p&gt;
&lt;p&gt;Check &lt;strong&gt;VPN &amp;gt; WireGuard &amp;gt; General&lt;/strong&gt; and make sure the service is enabled and running. If it&amp;rsquo;s enabled but &lt;code&gt;wg0&lt;/code&gt; still doesn&amp;rsquo;t exist, try toggling it off and on again. Check &lt;strong&gt;System &amp;gt; Log Files &amp;gt; General&lt;/strong&gt; for errors.&lt;/p&gt;
&lt;h3 id="vps-sees-replies-from-1010102-instead-of-the-lan-ip"&gt;VPS Sees Replies From 10.10.10.2 Instead of the LAN IP
&lt;/h3&gt;&lt;p&gt;This means outbound NAT is rewriting the source address on the WireGuard interface. For a reverse proxy setup, you don&amp;rsquo;t need outbound NAT. Remove it if it&amp;rsquo;s there. The VPS has a route for your LAN subnet, so replies from Jellyfin&amp;rsquo;s LAN IP will route back through the tunnel without NAT.&lt;/p&gt;
&lt;p&gt;If you haven&amp;rsquo;t manually added outbound NAT and you&amp;rsquo;re still seeing this, OPNsense&amp;rsquo;s automatic outbound NAT might be generating a rule for the WireGuard interface. Go to &lt;strong&gt;Firewall &amp;gt; NAT &amp;gt; Outbound&lt;/strong&gt;, switch to Hybrid mode, and verify no auto-generated rules are matching tunnel traffic. If one is, switch to Manual mode or add a manual rule that explicitly excludes the WireGuard interface.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need policy routing for a VPS reverse proxy setup?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Policy routing is for when LAN devices initiate traffic that needs to exit through the tunnel. In this setup, the VPS initiates. Replies go back through the tunnel naturally because the destination (&lt;code&gt;10.10.10.1&lt;/code&gt;) is in the tunnel subnet.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need outbound NAT on the WireGuard interface?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not for this setup. The VPS already has a route for your LAN subnet. Replies from Jellyfin route back through the tunnel without NAT.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need a gateway defined for the WireGuard interface?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not strictly. Gateways are needed for policy routing, where you&amp;rsquo;re forcing LAN traffic out through the tunnel. Since the VPS initiates connections and replies route back naturally, OPNsense doesn&amp;rsquo;t need a gateway to make routing decisions.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I add more services behind the VPS?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;Add a firewall rule on the &lt;code&gt;RackNerdVPS&lt;/code&gt; interface for each new service, specifying the exact destination IP and port. Or create an alias with all your allowed hosts and use that in a single rule (see Step 4 above). Don&amp;rsquo;t widen an existing rule to pass all traffic. Each service should be explicitly allowed.&lt;/p&gt;
&lt;p&gt;On the VPS side, add a new &lt;code&gt;reverse_proxy&lt;/code&gt; block in your Caddyfile (or &lt;code&gt;server&lt;/code&gt; block in nginx) pointing to the new service&amp;rsquo;s LAN IP and port.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ AllowedIPs 0.0.0.0/0 vs 10.10.10.0/24?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For a reverse proxy setup, &lt;code&gt;10.10.10.0/24&lt;/code&gt; is correct. &lt;code&gt;0.0.0.0/0&lt;/code&gt; tells OPNsense to send &lt;em&gt;all&lt;/em&gt; traffic through the tunnel, which is a full-tunnel setup. Different use case, different guide, different headaches.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What if I also want some LAN devices to use the VPS as their internet gateway?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;That&amp;rsquo;s a full-tunnel setup and requires additional configuration: changing AllowedIPs to &lt;code&gt;0.0.0.0/0&lt;/code&gt;, creating a gateway, adding policy-based routing rules on LAN, and setting up outbound NAT on the WireGuard interface. It&amp;rsquo;s doable, but it&amp;rsquo;s a separate project. Don&amp;rsquo;t try to bolt it onto this guide. You&amp;rsquo;ll break what&amp;rsquo;s already working.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What MTU should I use?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Start with &lt;code&gt;1420&lt;/code&gt; on bare metal, &lt;code&gt;1412&lt;/code&gt; on PPPoE, or &lt;code&gt;1380&lt;/code&gt; if OPNsense is virtualized. Test with a do-not-fragment ping. If Jellyfin streams stall on large video files but the web UI loads fine, your MTU is too high. See the MTU Tuning section for the full procedure.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is my home IP exposed in this setup?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Internet clients connect to your VPS public IP. Caddy handles TLS termination and reverse proxies through the tunnel. The client never sees your home IP. Your WAN IP is only used for the WireGuard handshake to the VPS, which is encrypted UDP traffic on port 51820. An observer would see your home IP connecting to the VPS, but they can&amp;rsquo;t see what&amp;rsquo;s inside the tunnel or what services you&amp;rsquo;re running.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;A VPS reverse proxy setup through WireGuard is one of the simplest tunnel configurations you can run. The pieces that matter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard instance and peer configured on OPNsense&lt;/li&gt;
&lt;li&gt;Interface assigned so firewall rules can attach to it&lt;/li&gt;
&lt;li&gt;A tight firewall rule that only allows VPS traffic to your specific services&lt;/li&gt;
&lt;li&gt;A route and AllowedIPs entry on the VPS so it knows your LAN subnet lives behind the tunnel&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Get those four things right and everything works. Get the firewall rule lazy (pass any to any) and you&amp;rsquo;ve handed anyone who compromises your VPS a direct path to your entire LAN. Don&amp;rsquo;t do that. I&amp;rsquo;ve seen the aftermath, and it&amp;rsquo;s not a fun weekend.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re building a serious homelab, this setup unlocks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Secure VPS reverse proxy without exposing home ports&lt;/li&gt;
&lt;li&gt;Multiple services behind a single VPS IP&lt;/li&gt;
&lt;li&gt;TLS termination at the VPS edge&lt;/li&gt;
&lt;li&gt;Hidden home IP for all exposed services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next step: experiment with adding more services behind the tunnel and segmenting them with per-service firewall rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is Part 3 of a 3-part series on building a VPS-fronted homelab.&lt;/strong&gt; If you landed here first, start with the why and the tunnel build:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/featured_hu_4aa109eb7ea649fe.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/featured_hu_5cd58cfc595ca82c.webp" width="100" height="100" alt="Why Your Homelab Needs a VPS to Share Services Publicly" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 1: Why Your Homelab Needs a VPS&lt;/strong&gt;
Why a VPS edge beats home port forwarding for public-facing services.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 1
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/featured_hu_de8a9ae15c68c1b0.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/featured_hu_c13d56a28e255655.webp" width="100" height="100" alt="How to Install WireGuard on a VPS and Connect It to Your Homelab" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 2: Install WireGuard on a VPS&lt;/strong&gt;
Build the VPS-to-homelab WireGuard tunnel with iptables port forwarding and a Caddy reverse proxy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 2
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Part 3: WireGuard Client on OPNsense and pfSense: LAN Routing for Your VPS Tunnel (this post)&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>WireGuard VPS to Homelab Tunnel: Port Forwarding + Caddy</title><link>https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/</link><pubDate>Thu, 05 Mar 2026 07:23:42 +0000</pubDate><guid>https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/featured_hu_fc0d79dc3b66495f.webp" alt="Featured image of post WireGuard VPS to Homelab Tunnel: Port Forwarding + Caddy" /&gt;&lt;p&gt;Exposing your homelab to the internet is risky. Port forwarding your home router, poking holes in firewalls, juggling dynamic IPs, dealing with CGNAT. It gets messy fast.&lt;/p&gt;
&lt;p&gt;A cleaner approach: install WireGuard on a cheap VPS, create a secure tunnel back to your homelab, and let the VPS act as your public gateway. From there, layer in a Caddy reverse proxy for HTTPS, lock down the VPS with iptables.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve done this a few times now. Getting WireGuard running was the easy part. The part that drove me insane was an MTU mismatch silently killing large packet transfers. The fix was two lines. &lt;em&gt;Finding&lt;/em&gt; it was the hard part.&lt;/p&gt;
&lt;p&gt;This guide walks through the VPS setup, including the parts that usually break.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
&lt;br&gt;
Install WireGuard on your VPS. Lock it down with iptables. Test the tunnel with a temporary Linux client. Use Caddy on the VPS to reverse proxy traffic through the tunnel to your homelab services. Tune MTU if large transfers stall or don't work at all. Permanent homelab client setup on OPNsense/pfSense is in Part 3.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="architecture-overview-vps-gateway--homelab-client"&gt;Architecture Overview: VPS Gateway + Homelab Client
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what we&amp;rsquo;re building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VPS (public IP)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard server&lt;/li&gt;
&lt;li&gt;Caddy reverse proxy (handles TLS and proxies to homelab through the tunnel)&lt;/li&gt;
&lt;li&gt;iptables host firewall&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homelab (behind NAT or CGNAT)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard client (OPNsense/pfSense setup in Part 3)&lt;/li&gt;
&lt;li&gt;Local services (Jellyfin, Immich, dashboards, etc.)&lt;/li&gt;
&lt;li&gt;Optional internal reverse proxy (Nginx, Traefik, Caddy)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Traffic flow:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Internet -&amp;gt; VPS -&amp;gt; External Reverse Proxy -&amp;gt; WireGuard tunnel -&amp;gt; Homelab service
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
Caddy terminates the incoming connection and opens a new one to your homelab through the tunnel. This is proxied traffic, not forwarded traffic. The kernel&amp;rsquo;s FORWARD chain never sees it. That means the base setup needs no FORWARD rules, no NAT, and no IP forwarding. The iptables firewall only needs to protect the VPS itself.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Your home network stays private. No router port forwarding required.&lt;/p&gt;
&lt;h2 id="prepare-the-vps-debian-13"&gt;Prepare the VPS (Debian 13)
&lt;/h2&gt;&lt;p&gt;This guide assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debian 13 is installed on your VPS. Ubuntu 24.04 or 22.04 works too with the same commands.&lt;/li&gt;
&lt;li&gt;First login is with root&lt;/li&gt;
&lt;li&gt;No other users have been created&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="update-the-system-and-install-required-packages"&gt;Update the System and Install Required Packages
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade -y &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt install sudo iptables -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="create-a-new-user"&gt;Create a New User
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;adduser &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the user to the sudo group&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;usermod -aG sudo &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify they are in the sudo group:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should return this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;username&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;username&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;username&lt;span class="o"&gt;)&lt;/span&gt;,27&lt;span class="o"&gt;(&lt;/span&gt;sudo&lt;span class="o"&gt;)&lt;/span&gt;,100&lt;span class="o"&gt;(&lt;/span&gt;users&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can see that group ID 27 (sudo) was added.&lt;/p&gt;
&lt;h3 id="install-wireguard"&gt;Install WireGuard
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install wireguard -y &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chmod &lt;span class="m"&gt;770&lt;/span&gt; /etc/wireguard/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; chown root:&amp;lt;username&amp;gt; /etc/wireguard/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="switch-from-root-to-the-normal-user-with-sudo-rights"&gt;Switch from root to the normal user with sudo rights
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;su &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="generate-wireguard-keys"&gt;Generate WireGuard Keys
&lt;/h2&gt;&lt;p&gt;On the VPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/wireguard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo wg genkey &lt;span class="p"&gt;|&lt;/span&gt; sudo tee server_private.key &lt;span class="p"&gt;|&lt;/span&gt; wg pubkey &amp;gt; server_public.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server_private.key&lt;/code&gt; stays secret. Never paste it into chat, docs, or ticket systems. Not even &amp;ldquo;only for a second.&amp;rdquo; Keys leak. They always leak.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server_public.key&lt;/code&gt; will be shared with the homelab.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lock down the private key immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod &lt;span class="m"&gt;600&lt;/span&gt; server_private.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
If you skip this, anyone with shell access to that box can read your tunnel credentials. Don&amp;rsquo;t skip this.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="configure-the-vps-wireguard-server"&gt;Configure the VPS WireGuard Server
&lt;/h2&gt;&lt;p&gt;Create the config file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/wireguard/wg0.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.1/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server_private_key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ListenPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. No PostUp, no PostDown, no iptables spaghetti crammed into a WireGuard config. The firewall is managed separately, which is where it belongs.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Avoid overlapping subnets. If your home LAN is &lt;code&gt;10.0.0.0/24&lt;/code&gt;, pick something else like &lt;code&gt;10.13.13.0/24&lt;/code&gt;. Overlapping subnets cause traffic to stay local instead of hitting the tunnel, and the resulting behavior looks like the tunnel is broken when it&amp;rsquo;s actually a routing conflict.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="secure-the-vps-with-iptables"&gt;Secure the VPS with iptables
&lt;/h2&gt;&lt;p&gt;Your VPS is on the public internet. Lock it down before you do anything else.&lt;/p&gt;
&lt;p&gt;These rules control what traffic the VPS accepts &lt;em&gt;for itself&lt;/em&gt; (the INPUT chain). Since Caddy proxies traffic rather than forwarding it, these INPUT rules are all you need for the base setup.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -i lo -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -p icmp -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -p tcp --dport &lt;span class="m"&gt;22&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -p tcp --dport &lt;span class="m"&gt;80&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -p tcp --dport &lt;span class="m"&gt;443&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -A INPUT -p udp --dport &lt;span class="m"&gt;51820&lt;/span&gt; -j ACCEPT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -P INPUT DROP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What each of these do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Accept return traffic for existing connections&lt;/li&gt;
&lt;li&gt;Accept loopback (local services talking to each other)&lt;/li&gt;
&lt;li&gt;Accept ICMP (ping, path MTU discovery)&lt;/li&gt;
&lt;li&gt;Accept SSH&lt;/li&gt;
&lt;li&gt;Accept HTTP&lt;/li&gt;
&lt;li&gt;Accept HTTPS&lt;/li&gt;
&lt;li&gt;Accept WireGuard&lt;/li&gt;
&lt;li&gt;Drop everything else&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Rule order matters here. The conntrack rule goes first because it handles the bulk of traffic in a single check. Everything after that is for new inbound connections only. The policy DROP at the end catches anything you didn&amp;rsquo;t explicitly allow.&lt;/p&gt;
&lt;p&gt;Persist across reboots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install iptables-persistent -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo netfilter-persistent save
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify your rules are in place:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo iptables -L INPUT -n -v --line-numbers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your rules should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Chain INPUT (policy DROP 3 packets, 171 bytes)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;num pkts bytes target prot opt in out source destination &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;1 7 492 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;2 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;3 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;4 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;5 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;6 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;7 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see your rules numbered in order. If you lock yourself out of SSH, most VPS providers have a web console or rescue mode. Test SSH access in a second terminal before you close your current session.&lt;/p&gt;
&lt;h3 id="a-note-on-firewall-coexistence"&gt;A Note on Firewall Coexistence
&lt;/h3&gt;&lt;p&gt;These iptables rules assume a clean firewall. If you&amp;rsquo;re on a fresh VPS from RackNerd, Hetzner, Linode, Vultr, or DigitalOcean with no custom firewall configured, you&amp;rsquo;re good.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve customized your firewall or aren&amp;rsquo;t sure, check what&amp;rsquo;s running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw status
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nft list ruleset
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;If UFW is active&lt;/strong&gt;: Disable it or manage your rules through UFW instead. Running both UFW and raw iptables rules leads to confusion. Pick one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If nftables is running&lt;/strong&gt;: On many VPS images, &lt;code&gt;iptables&lt;/code&gt; is actually a compatibility layer over nftables. Your commands will still work, but it&amp;rsquo;s worth knowing what&amp;rsquo;s underneath. Consider writing your rules in nft syntax instead if you prefer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloud provider firewalls&lt;/strong&gt;: Check your provider&amp;rsquo;s security group or firewall panel separately. AWS, Oracle Cloud, and Hetzner Cloud all have external firewalls that block traffic regardless of what you configure on the box itself. You&amp;rsquo;ll need to allow UDP 51820, TCP 80, TCP 443, and TCP 22 there too. This trips people up all the time. You can have perfect iptables rules and still get nothing because the cloud firewall is dropping it upstream.&lt;/p&gt;
&lt;h3 id="start-wireguard"&gt;Start WireGuard
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg-quick up wg0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; wg-quick@wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg show
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;interface: wg0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; public key: &amp;lt;Server_Public_Key&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; private key: &lt;span class="o"&gt;(&lt;/span&gt;hidden&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; listening port: &lt;span class="m"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If this shows your interface and listening port, the VPS side is ready. If it shows nothing, go back and check that &lt;code&gt;wg0.conf&lt;/code&gt; has valid syntax and your private key file is readable.&lt;/p&gt;
&lt;h2 id="test-the-tunnel-with-a-temporary-client"&gt;Test the Tunnel with a Temporary Client
&lt;/h2&gt;&lt;p&gt;Before configuring your firewall appliance as the permanent WireGuard client (covered in &lt;a class="link" href="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/" &gt;Part 3&lt;/a&gt;), verify the VPS side works with a quick throwaway Linux client. Any machine on your homelab network will do. You need something that can run WireGuard long enough to confirm the tunnel is alive.&lt;/p&gt;
&lt;p&gt;Install WireGuard and generate keys:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install wireguard -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wg genkey &lt;span class="p"&gt;|&lt;/span&gt; tee client_private.key &lt;span class="p"&gt;|&lt;/span&gt; wg pubkey &amp;gt; client_public.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; client_private.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Copy the contents of &lt;code&gt;client_public.key&lt;/code&gt; and paste it into the &lt;code&gt;[Peer]&lt;/code&gt; section of your VPS &lt;code&gt;wg0.conf&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat client_public.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the VPS, paste the key into &lt;code&gt;wg0.conf&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/wireguard/wg0.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.1/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;Server_Private_Key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ListenPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PublicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;Client_Public_Key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AllowedIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.2/32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then restart WireGuard on the VPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg-quick down wg0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg-quick up wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create a minimal config on the test client:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/wireguard/wg0.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Interface]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.2/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;Client_Private_key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;MTU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1420&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Peer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PublicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;Server_Public_Key&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;VPS_Public_IP&amp;gt;:51820&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AllowedIPs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.10.0/24&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; is required if your homelab is behind NAT or CGNAT. Without it, the NAT mapping expires, and the tunnel goes silent. If you&amp;rsquo;re not sure whether you need it, you need it.&lt;/p&gt;
&lt;p&gt;Bring the tunnel up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg-quick up wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Don&amp;rsquo;t enable it as a service. This is a test, not the permanent setup.&lt;/p&gt;
&lt;p&gt;Now verify from both sides. On the test client:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping 10.10.10.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the VPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping 10.10.10.2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check the handshake:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg show
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it is working it should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;interface: wg0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; public key: &amp;lt;Client_Public_Key&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; private key: &lt;span class="o"&gt;(&lt;/span&gt;hidden&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; listening port: &lt;span class="m"&gt;45365&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;peer: &amp;lt;Server_Public_Key&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; endpoint: &amp;lt;VPS IP&amp;gt;:51820
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allowed ips: 10.10.10.0/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; latest handshake: &lt;span class="m"&gt;1&lt;/span&gt; minute, &lt;span class="m"&gt;10&lt;/span&gt; seconds ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; transfer: 1.27 KiB received, 2.08 KiB sent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; persistent keepalive: every &lt;span class="m"&gt;25&lt;/span&gt; seconds
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see a recent handshake timestamp. If you don&amp;rsquo;t, jump to the troubleshooting section below.&lt;/p&gt;
&lt;p&gt;If pings work in both directions, the VPS side is solid. Tear down the test client when you&amp;rsquo;re done:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg-quick down wg0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I will cover a permanent client setup on OPNsense or pfSense in next week&amp;rsquo;s post, including interface assignment, gateway creation, and LAN routing so all devices on your network can reach the tunnel.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
A compact mini-workstation built around up to a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. Great as a homelab node or small server with serious I/O.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="set-up-caddy-on-the-vps"&gt;Set Up Caddy on the VPS
&lt;/h2&gt;&lt;p&gt;Now expose services safely using Caddy as a reverse proxy. Caddy is installed directly on the VPS as a system package. No Docker required. It binds to ports 80 and 443 on the host, handles Let&amp;rsquo;s Encrypt certificates automatically, and proxies traffic through the WireGuard tunnel to your homelab. No bridge networks, no volume mounts, no container restarts to debug. One less layer between the internet and your services.&lt;/p&gt;
&lt;h3 id="install-caddy"&gt;Install Caddy
&lt;/h3&gt;&lt;p&gt;Add the official Caddy repository and install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -1sLf &lt;span class="s1"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -1sLf &lt;span class="s1"&gt;&amp;#39;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo tee /etc/apt/sources.list.d/caddy-stable.list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install caddy -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This installs Caddy as a systemd service. It starts automatically and runs as the &lt;code&gt;caddy&lt;/code&gt; user. Verify it&amp;rsquo;s running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;active (running)&lt;/code&gt;. If it failed, check the journal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u caddy --no-pager -n &lt;span class="m"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="configure-the-caddyfile"&gt;Configure the Caddyfile
&lt;/h3&gt;&lt;p&gt;The default Caddyfile lives at &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;. Open it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/caddy/Caddyfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the contents with your reverse proxy config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddy" data-lang="caddy"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;flix.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;10.10.10.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8096&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="se"&gt;{host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{remote_host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;flush_interval&lt;/span&gt; &lt;span class="mi"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;transport&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;read_buffer&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;write_buffer&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;dial_timeout&lt;/span&gt; &lt;span class="mi"&gt;30s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;response_header_timeout&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;photos.example.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;10.10.10.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2283&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="se"&gt;{host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;header_up&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="se"&gt;{remote_host}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After editing, reload Caddy to pick up the changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reload caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Use &lt;code&gt;reload&lt;/code&gt;, not &lt;code&gt;restart&lt;/code&gt;. A reload validates the new config first and keeps the old config running if validation fails. A restart kills the process and hopes for the best. Get in the habit of using reload.&lt;/p&gt;
&lt;p&gt;This forwards HTTPS traffic from the internet to the VPS, through WireGuard, to your homelab services.&lt;/p&gt;
&lt;p&gt;Caddy handles Let&amp;rsquo;s Encrypt certificates automatically. Make sure each subdomain has a DNS A record pointing to the VPS public IP before you reload. Caddy will try to provision certificates immediately and will fail if DNS isn&amp;rsquo;t ready. Check &lt;code&gt;journalctl -u caddy&lt;/code&gt; if certificates aren&amp;rsquo;t appearing.&lt;/p&gt;
&lt;h3 id="preserving-client-ip"&gt;Preserving Client IP
&lt;/h3&gt;&lt;p&gt;By default, Caddy sets &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers. If your homelab service needs the real client IP (Jellyfin&amp;rsquo;s fail2ban integration, for example), make sure the internal service trusts the VPS tunnel address (&lt;code&gt;10.10.10.1&lt;/code&gt;) as a proxy. Without this, every request your services see will appear to come from the tunnel IP. Your access logs become useless and any IP-based rate limiting or banning breaks.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has more than enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without turning your closet into a jet engine or space heater.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="mtu-problems-the-silent-killer"&gt;MTU Problems: The Silent Killer
&lt;/h2&gt;&lt;p&gt;This cost me hours. It will cost you hours too if you don&amp;rsquo;t read this section before you need it.&lt;/p&gt;
&lt;p&gt;Symptoms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small pings work.&lt;/li&gt;
&lt;li&gt;HTTPS loads halfway, then stalls.&lt;/li&gt;
&lt;li&gt;File uploads freeze.&lt;/li&gt;
&lt;li&gt;No obvious errors in logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything &lt;em&gt;looks&lt;/em&gt; fine. Nothing &lt;em&gt;works&lt;/em&gt; fine. The issue is usually an MTU mismatch between your VPS provider and home ISP. Packets too large for the tunnel get silently dropped because ICMP &amp;ldquo;fragmentation needed&amp;rdquo; messages are blocked somewhere along the path. This is called a PMTU black hole, and it is exactly as fun as it sounds.&lt;/p&gt;
&lt;p&gt;WireGuard defaults to 1420. Some VPS networks require lower values.&lt;/p&gt;
&lt;h3 id="how-to-diagnose-mtu-issues"&gt;How to Diagnose MTU Issues
&lt;/h3&gt;&lt;p&gt;On homelab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping -M &lt;span class="k"&gt;do&lt;/span&gt; -s &lt;span class="m"&gt;1372&lt;/span&gt; 10.10.10.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Start at 1372 and increase by 10 until packets fail. The highest working value plus 28 bytes (IP + ICMP headers) is your effective MTU.&lt;/p&gt;
&lt;p&gt;If fragmentation fails above 1380, try:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;MTU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1280&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add it to both configs under &lt;code&gt;[Interface]&lt;/code&gt;, then restart WireGuard on both ends.&lt;/p&gt;
&lt;p&gt;This small change fixes a surprising number of &amp;ldquo;random&amp;rdquo; tunnel issues. If you&amp;rsquo;re seeing intermittent stalls on large transfers, check MTU first. Always MTU first.&lt;/p&gt;
&lt;p&gt;An MTU of 1380 is what worked best for me streaming Jellyfin on my RackNerd VPS.&lt;/p&gt;
&lt;h2 id="dual-reverse-proxy-architecture-advanced"&gt;Dual Reverse Proxy Architecture (Advanced)
&lt;/h2&gt;&lt;p&gt;For larger setups, a two-proxy approach keeps things clean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS: Caddy public-facing, handles TLS termination&lt;/li&gt;
&lt;li&gt;Homelab: Nginx, Traefik, or Caddy internally, handles service routing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flow:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Internet
|
VPS Caddy (TLS termination, public certificates)
|
WireGuard tunnel
|
Homelab Reverse Proxy (service routing, internal TLS optional)
|
Service containers
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSL termination at the VPS keeps certificate management in one place&lt;/li&gt;
&lt;li&gt;Adding new services means updating the homelab proxy only. The VPS side stays stable.&lt;/li&gt;
&lt;li&gt;Zero exposed home ports&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The VPS proxy passes &lt;code&gt;X-Forwarded-For&lt;/code&gt; and &lt;code&gt;X-Real-IP&lt;/code&gt; headers. Your homelab proxy should trust the tunnel source IP (&lt;code&gt;10.10.10.1&lt;/code&gt;) and preserve these headers for accurate logging.&lt;/p&gt;
&lt;h3 id="example-vps-caddy--homelab-nginx"&gt;Example: VPS Caddy + Homelab Nginx
&lt;/h3&gt;&lt;p&gt;On the VPS, edit &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; to point everything at the homelab&amp;rsquo;s internal reverse proxy on a single port:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-caddy" data-lang="caddy"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;jellyfin.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;10.10.10.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;nextcloud.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;10.10.10.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;dashboard.yourdomain.com&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;reverse_proxy&lt;/span&gt; &lt;span class="n"&gt;10.10.10.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then reload:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reload caddy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the homelab, Nginx listens on port 8080 and routes by hostname:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;jellyfin.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="n"&gt;10.10.10.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;real_ip_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:8096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;nextcloud.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="n"&gt;10.10.10.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;real_ip_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:8443&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The key lines are &lt;code&gt;set_real_ip_from 10.10.10.1&lt;/code&gt; and &lt;code&gt;real_ip_header X-Forwarded-For&lt;/code&gt;. Without these, every request your services see will appear to come from the tunnel IP instead of the real client.&lt;/p&gt;
&lt;p&gt;This pattern scales well. You add new services by updating your homelab proxy config. The VPS Caddyfile needs one more &lt;code&gt;reverse_proxy&lt;/code&gt; block pointing at the same homelab port.&lt;/p&gt;
&lt;h2 id="using-private-internet-access-wireguard"&gt;Using Private Internet Access WireGuard
&lt;/h2&gt;&lt;p&gt;If you already use &lt;strong&gt;Private Internet Access WireGuard&lt;/strong&gt;, do not reuse that interface for your VPS tunnel. I cannot stress this enough. Mixing them on the same interface causes conflicting routes that silently break one tunnel or the other, default route overrides that send your VPS traffic through PIA, and DNS conflicts where PIA&amp;rsquo;s settings clobber your homelab DNS.&lt;/p&gt;
&lt;p&gt;Keep them on separate interfaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;wg0&lt;/code&gt; for the VPS tunnel.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;wg1&lt;/code&gt; for PIA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need both active simultaneously, you&amp;rsquo;ll need policy routing with separate routing tables and &lt;code&gt;fwmark&lt;/code&gt; rules to keep traffic on the right path. That&amp;rsquo;s a full topic on its own and easy to get wrong. I&amp;rsquo;ll cover it in a dedicated post. For now, the important thing is: separate interfaces, separate routing tables, no stacking.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;
A fanless N150 appliance with six Intel i226-V 2.5GbE ports and hardware AES-NI, built to run OPNsense or pfSense. AES-NI is what keeps WireGuard throughput high without pinning the CPU, and the six ports give you somewhere to terminate the tunnel that isn&amp;rsquo;t your ISP router.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-checklist"&gt;Troubleshooting Checklist
&lt;/h2&gt;&lt;h3 id="no-handshake"&gt;No Handshake
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Is UDP 51820 open on the VPS? Check both the OS firewall (&lt;code&gt;sudo iptables -L INPUT -n -v&lt;/code&gt;) and the cloud provider&amp;rsquo;s security group.&lt;/li&gt;
&lt;li&gt;Correct public key on both sides? One wrong character and the handshake silently fails.&lt;/li&gt;
&lt;li&gt;Correct endpoint IP and port?&lt;/li&gt;
&lt;li&gt;NAT blocking outbound UDP from the homelab?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; set on the client?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo wg show
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If no &amp;ldquo;latest handshake&amp;rdquo; time appears, the two sides aren&amp;rsquo;t talking at all. This is a network-level issue, not a WireGuard config issue. Focus on firewalls, security groups, and port reachability.&lt;/p&gt;
&lt;h3 id="caddy-returns-502-bad-gateway"&gt;Caddy Returns 502 Bad Gateway
&lt;/h3&gt;&lt;p&gt;Caddy is running and serving certificates, but the proxied connection to your homelab is failing. This means the tunnel is probably up but the service on the other end isn&amp;rsquo;t reachable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can the VPS ping the homelab? &lt;code&gt;ping 10.10.10.2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Is the homelab service listening on the expected port? SSH into your homelab and check with &lt;code&gt;ss -tlnp | grep &amp;lt;port&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Firewall on the homelab blocking inbound connections from the tunnel? Check &lt;code&gt;iptables -L INPUT -n -v&lt;/code&gt; on the homelab side.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="caddy-not-serving-or-certificates-failing"&gt;Caddy Not Serving or Certificates Failing
&lt;/h3&gt;&lt;p&gt;Check status and logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status caddy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u caddy --no-pager -n &lt;span class="m"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Common causes: DNS A records not pointing at the VPS yet, port 80 or 443 already in use by another process (check with &lt;code&gt;sudo ss -tlnp | grep ':80\|:443'&lt;/code&gt;), or a syntax error in the Caddyfile. Validate your config before reloading:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;caddy validate --config /etc/caddy/Caddyfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="high-latency-or-slow-throughput"&gt;High Latency or Slow Throughput
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Check MTU first. Seriously. I said this already. I&amp;rsquo;ll say it again.&lt;/li&gt;
&lt;li&gt;Ensure no overlapping subnets between tunnel and LAN.&lt;/li&gt;
&lt;li&gt;Avoid double NAT when possible.&lt;/li&gt;
&lt;li&gt;Try lowering MTU to 1280 and test again.&lt;/li&gt;
&lt;li&gt;Check VPS provider for bandwidth caps or throttling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I forward specific ports from VPS to homelab over WireGuard?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Two options. For HTTP and HTTPS, use Caddy as a reverse proxy. It handles TLS, routing, and is simpler to manage.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need IP forwarding enabled?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The Caddy reverse proxy setup does not need IP forwarding because Caddy terminates connections locally and creates new ones through the tunnel. The kernel never forwards packets between interfaces in that case.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why is my WireGuard tunnel dropping packets?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Most of the time this is MTU. Lower MTU to somewhere between 1280 and 1400 and test with &lt;code&gt;ping -M do&lt;/code&gt;. If that fixes it, you&amp;rsquo;ve found your problem.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need PersistentKeepalive?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, if the homelab is behind NAT or CGNAT. Set it to 25 seconds. Without it, the NAT translation table entry expires and incoming packets get dropped.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I run Caddy on the VPS and Nginx on my homelab?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. This is the dual reverse proxy architecture described above. The VPS handles public TLS. The homelab proxy handles internal service routing. It&amp;rsquo;s the setup I recommend for homelabs with more than two or three exposed services.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I generate and exchange keys securely?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Generate keys locally on each machine. Share only public keys. Transfer them over SSH or a secure channel. Never paste private keys into chat apps, shared documents, or ticket systems. If a private key has ever been visible to anyone or anything you don&amp;rsquo;t control, generate a new one.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;WireGuard port forwarding through a VPS is one of the cleanest ways to expose your homelab to the internet.&lt;/p&gt;
&lt;p&gt;The core steps covered in this post:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install WireGuard on the VPS.&lt;/li&gt;
&lt;li&gt;Generate keys.&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;wg0.conf&lt;/code&gt; on the VPS.&lt;/li&gt;
&lt;li&gt;Lock down the VPS with iptables.&lt;/li&gt;
&lt;li&gt;Test the tunnel with a temporary Linux client.&lt;/li&gt;
&lt;li&gt;Install Caddy and configure reverse proxy for HTTPS.&lt;/li&gt;
&lt;li&gt;Fix MTU if large traffic stalls.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Next up: &lt;a class="link" href="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/" &gt;Part 3&lt;/a&gt; covers setting up the permanent WireGuard client on OPNsense or pfSense with LAN routing so every device on your network can reach the tunnel.&lt;/p&gt;
&lt;p&gt;The hardest part is rarely WireGuard itself. It&amp;rsquo;s understanding which traffic flows through which iptables chains, and MTU edge cases. Once you understand those pieces, the rest falls into place.&lt;/p&gt;
&lt;p&gt;If you want to go further:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add &lt;a class="link" href="https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/" &gt;fail2ban on the VPS&lt;/a&gt; for SSH and service protection.&lt;/li&gt;
&lt;li&gt;Use Cloudflare DNS with proxy mode for DDoS protection.&lt;/li&gt;
&lt;li&gt;Add a second VPS for high availability.&lt;/li&gt;
&lt;li&gt;Implement policy routing with multiple WireGuard tunnels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Secure tunnels beat router port forwarding every time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is Part 2 of a 3-part series on building a VPS-fronted homelab.&lt;/strong&gt; For the why behind this architecture, and how to route the tunnel into your LAN, see the other parts:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/featured_hu_4aa109eb7ea649fe.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/featured_hu_5cd58cfc595ca82c.webp" width="100" height="100" alt="Why Your Homelab Needs a VPS to Share Services Publicly" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 1: Why Your Homelab Needs a VPS&lt;/strong&gt;
Why a VPS edge beats home port forwarding for public-facing services.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 1
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/featured_hu_967326aab8997f75.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/featured_hu_ea64966e411a3d5b.webp" width="100" height="100" alt="WireGuard Client on OPNsense and pfSense: LAN Routing for Your VPS Tunnel" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 3: Route the Tunnel to Your LAN&lt;/strong&gt;
Set up the WireGuard client on OPNsense or pfSense so every device on your network reaches the tunnel.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 3
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
A palm-sized single-board computer with a quad-core ARM CPU and 4GB RAM that runs full Linux, with Gigabit Ethernet, dual-band Wi-Fi, USB 3.0, and dual micro-HDMI. Ideal for lightweight homelab duties like running a WireGuard endpoint, Pi-hole, or small Docker stacks while sipping a few watts.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>VPS + WireGuard: Why Your Homelab Needs a Public Edge</title><link>https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/</link><pubDate>Wed, 04 Mar 2026 07:42:23 +0000</pubDate><guid>https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/vps-wireguard-homelab-private-internet-access/featured_hu_16c4d745089d567e.webp" alt="Featured image of post VPS + WireGuard: Why Your Homelab Needs a Public Edge" /&gt;&lt;p&gt;You finally got your homelab dialed in. Jellyfin streams flawlessly. Your dashboards are organized. Maybe you&amp;rsquo;re even self-hosting Gitea, Nextcloud, or a personal blog.&lt;/p&gt;
&lt;p&gt;Then comes the big question: how do you safely share those services with the outside world?&lt;/p&gt;
&lt;p&gt;If your answer is port forwarding on your router, you&amp;rsquo;re not alone. I did the same thing. I slapped a reverse proxy and Cloudflare in front and called it a day. It worked. It felt secure. It was not as secure as I thought.&lt;/p&gt;
&lt;p&gt;When I moved to a Virtual Private Server (VPS) running a second reverse proxy with a WireGuard tunnel back to my homelab, everything changed. My home IP disappeared from the public internet. My router stopped getting hammered. I stopped lying awake wondering what was poking at my network.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re serious about running public-facing services from home, a VPS in front of your homelab is the smarter move. Not because port forwarding is automatically a problem, but because a VPS shrinks your attack surface and limits the blast radius when something goes wrong. And something always goes wrong eventually.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
&lt;br&gt;
Direct port forwarding exposes your home IP to constant scanning and increases your attack surface. A VPS acting as a reverse proxy with a WireGuard tunnel hides your home network, reduces blast radius, and gives you more control than relying only on Cloudflare or other zero trust tunnel solutions.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-real-problem-with-port-forwarding-at-home"&gt;The Real Problem With Port Forwarding at Home
&lt;/h2&gt;&lt;h3 id="your-attack-surface-is-bigger-than-you-think"&gt;Your Attack Surface Is Bigger Than You Think
&lt;/h3&gt;&lt;p&gt;When you forward ports on your router, you&amp;rsquo;re publishing your home IP address to the world. That&amp;rsquo;s not automatically a death sentence. ISPs rotate IPs, CGNAT exists, and plenty of people run services this way for years without incident. But every open port is another door you&amp;rsquo;re daring the internet to try.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bots can scan and fingerprint your services&lt;/li&gt;
&lt;li&gt;DDoS attempts can target your home connection directly&lt;/li&gt;
&lt;li&gt;Persistent probing never stops, even on non-standard ports&lt;/li&gt;
&lt;li&gt;A single misconfigured service becomes a direct entry point to your home network&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automated scanners are relentless. Within minutes of opening ports 80 or 443, you&amp;rsquo;ll see probes in your logs. Open 8096 for Jellyfin. Same story. The entire public IPv4 space gets scanned constantly, and your little corner of it is not special enough to be ignored.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s why that matters more for homelabs than enterprise. You likely don&amp;rsquo;t have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An IDS/IPS appliance doing deep packet inspection&lt;/li&gt;
&lt;li&gt;Upstream DDoS mitigation from your ISP&lt;/li&gt;
&lt;li&gt;A SOC team monitoring logs around the clock&lt;/li&gt;
&lt;li&gt;Redundant uplinks or failover&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your home internet connection is a single point of failure. If someone floods it, your family loses internet too. That&amp;rsquo;s not a hypothetical. That&amp;rsquo;s a Tuesday for anyone running public services on a residential line.&lt;/p&gt;
&lt;h3 id="the-myth-of-safe-port-forwarding"&gt;The Myth of &amp;ldquo;Safe Port Forwarding&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Some argue that best practices, diligent patching, and network isolation make you 99% safe. In theory, maybe.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero-days happen&lt;/li&gt;
&lt;li&gt;Misconfigurations happen&lt;/li&gt;
&lt;li&gt;Humans make mistakes&lt;/li&gt;
&lt;li&gt;Bots don&amp;rsquo;t sleep&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The issue isn&amp;rsquo;t only application security. It&amp;rsquo;s about reducing how much of your lab is reachable from the internet and limiting the damage when something inevitably slips through. Defense in depth isn&amp;rsquo;t paranoia. It&amp;rsquo;s plumbing.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;&lt;br&gt;
A fanless N150 appliance with six Intel i226-V 2.5GbE ports and hardware AES-NI, built for OPNsense or pfSense. AES-NI is what lets a box this small push WireGuard near line rate, and running the tunnel on the firewall rather than on one client puts your whole LAN behind it.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-a-vps-changes-for-your-homelab"&gt;What a VPS Changes for Your Homelab
&lt;/h2&gt;&lt;h3 id="a-vps-becomes-your-public-edge"&gt;A VPS Becomes Your Public Edge
&lt;/h3&gt;&lt;p&gt;A Virtual Private Server acts as your public-facing gateway. Instead of traffic going straight to your house, it hits the VPS first.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Internet → Your Home Router → Homelab
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You get:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Internet → VPS → WireGuard Tunnel → Homelab
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The world only sees the VPS IP address. Your home IP is never published. That one change shifts your entire risk profile.&lt;/p&gt;
&lt;h3 id="your-home-ip-stays-private"&gt;Your Home IP Stays Private
&lt;/h3&gt;&lt;p&gt;All traffic terminates at the VPS first. The VPS forwards it over an encrypted WireGuard tunnel to your homelab.&lt;/p&gt;
&lt;p&gt;From the outside:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS resolves to the VPS IP&lt;/li&gt;
&lt;li&gt;Nmap scans hit the VPS&lt;/li&gt;
&lt;li&gt;Attack traffic hits the VPS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If someone decides to DDoS your public endpoint, they&amp;rsquo;re punching the VPS, not your home router. Your family&amp;rsquo;s Netflix and your kid&amp;rsquo;s Minecraft session keep working.&lt;/p&gt;
&lt;h3 id="no-more-port-forwarding-at-home"&gt;No More Port Forwarding at Home
&lt;/h3&gt;&lt;p&gt;With the VPS plus WireGuard model, your home router stays locked down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No forwarding ports 80 or 443&lt;/li&gt;
&lt;li&gt;No exposing random service ports&lt;/li&gt;
&lt;li&gt;No inbound NAT rules on your residential gateway&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only the VPS exposes ports publicly. On the home side, the WireGuard tunnel is outbound-initiated. Your homelab reaches out to the VPS, not the other way around. No inbound firewall rules needed on your router. That alone is a massive reduction in exposure.&lt;/p&gt;
&lt;h2 id="why-cloudflare-alternatives-matter"&gt;Why Cloudflare Alternatives Matter
&lt;/h2&gt;&lt;p&gt;I used Cloudflare. Many homelabbers do.&lt;/p&gt;
&lt;p&gt;Cloudflare Tunnel and Zero Trust solutions avoid open ports at home. That&amp;rsquo;s a real improvement over raw port forwarding. But there are trade-offs worth understanding before you go all-in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No ports open at home&lt;/li&gt;
&lt;li&gt;Easy setup&lt;/li&gt;
&lt;li&gt;Automatic HTTPS&lt;/li&gt;
&lt;li&gt;Built-in DDoS mitigation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="tls-termination-and-trust"&gt;TLS Termination and Trust
&lt;/h3&gt;&lt;p&gt;Cloudflare terminates TLS. That means Cloudflare decrypts your traffic and re-encrypts it to your origin. For hobby projects, that&amp;rsquo;s usually fine.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the trade-off: any reverse proxy you don&amp;rsquo;t control can see your decrypted traffic. That includes Cloudflare. It also includes a VPS provider if they wanted to snapshot RAM on the host, though that&amp;rsquo;s a different threat model. Both approaches involve trusting a third-party infrastructure provider. With a VPS you control, you at least decide where TLS terminates and you manage the certificates yourself.&lt;/p&gt;
&lt;p&gt;You own the edge.&lt;/p&gt;
&lt;h3 id="vendor-lock-in"&gt;Vendor Lock-In
&lt;/h3&gt;&lt;p&gt;Cloudflare is excellent. It&amp;rsquo;s still a vendor. If you hit bandwidth limits, violate ToS unintentionally, need advanced routing, or want to proxy non-HTTP services, you can run into walls fast.&lt;/p&gt;
&lt;p&gt;High-bandwidth media streaming is especially risky. Depending on your plan and usage, Cloudflare may throttle or flag you. Streaming terabytes of Jellyfin through their free tier is not what they had in mind.&lt;/p&gt;
&lt;p&gt;With a VPS, you&amp;rsquo;re not tied to one company&amp;rsquo;s proxy layer. You can move providers without redesigning your entire network.&lt;/p&gt;
&lt;h3 id="non-http-services"&gt;Non-HTTP Services
&lt;/h3&gt;&lt;p&gt;Cloudflare excels at HTTP and HTTPS. It is not built for everything else.&lt;/p&gt;
&lt;p&gt;What about custom TCP services, game servers, experimental protocols, or self-hosted APIs over raw TCP? A VPS gives you full Layer 4 and Layer 7 control. You can proxy HTTP, HTTPS, TCP, UDP, and anything else WireGuard can tunnel.&lt;/p&gt;
&lt;p&gt;That flexibility matters the moment you move beyond basic web apps.&lt;/p&gt;
&lt;h2 id="the-vps--wireguard-architecture-overview"&gt;The VPS + WireGuard Architecture Overview
&lt;/h2&gt;&lt;p&gt;No install steps here (the next post covers that). This is the lay of the land.&lt;/p&gt;
&lt;h3 id="the-components"&gt;The Components
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VPS in the cloud&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static public IP&lt;/li&gt;
&lt;li&gt;Reverse proxy such as Caddy or Nginx&lt;/li&gt;
&lt;li&gt;Strict firewall rules (only ports 80, 443, and your WireGuard UDP port)&lt;/li&gt;
&lt;li&gt;WireGuard server&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Homelab server&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard client (initiates the tunnel outbound)&lt;/li&gt;
&lt;li&gt;Services bound to private interfaces only&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Secure tunnel&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encrypted point-to-point VPN between VPS and homelab&lt;/li&gt;
&lt;li&gt;VPS routes inbound requests over the tunnel to your services&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="how-traffic-flows"&gt;How Traffic Flows
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;User visits &lt;code&gt;app.yourdomain.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DNS points to the VPS public IP&lt;/li&gt;
&lt;li&gt;VPS reverse proxy receives the request&lt;/li&gt;
&lt;li&gt;VPS forwards the request over WireGuard to the homelab&lt;/li&gt;
&lt;li&gt;Homelab responds through the tunnel&lt;/li&gt;
&lt;li&gt;VPS returns the response to the user&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At no point is your home IP exposed publicly.&lt;/p&gt;
&lt;p&gt;If someone runs:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nmap yourdomain.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;They only see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPS ports&lt;/li&gt;
&lt;li&gt;VPS fingerprint&lt;/li&gt;
&lt;li&gt;VPS OS signature&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not your home network. Not your ISP. Nothing useful.&lt;/p&gt;
&lt;h3 id="security-layers-you-gain"&gt;Security Layers You Gain
&lt;/h3&gt;&lt;p&gt;On the VPS you can stack additional defenses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rate limiting (via Caddy&amp;rsquo;s &lt;code&gt;rate_limit&lt;/code&gt; directive or Nginx&amp;rsquo;s &lt;code&gt;limit_req&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fail2ban watching your proxy logs&lt;/li&gt;
&lt;li&gt;Basic WAF rules&lt;/li&gt;
&lt;li&gt;Geo-blocking at the firewall level&lt;/li&gt;
&lt;li&gt;Strict &lt;code&gt;ufw&lt;/code&gt; or &lt;code&gt;nftables&lt;/code&gt; rules allowing only ports 80, 443, and your WireGuard UDP port&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In most cases, you only need ports 80 and 443 open publicly. Restrict WireGuard&amp;rsquo;s UDP port to known peer IPs where possible with something like &lt;code&gt;ufw allow from &amp;lt;home-ip&amp;gt; to any port 51820&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Your homelab becomes private infrastructure behind a single hardened edge node.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;&lt;br&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has more than enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without turning your closet into a jet engine or space heater.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="trade-offs-you-should-understand"&gt;Trade-Offs You Should Understand
&lt;/h2&gt;&lt;p&gt;A VPS is not magic. It adds responsibility. Go in with your eyes open.&lt;/p&gt;
&lt;h3 id="cost"&gt;Cost
&lt;/h3&gt;&lt;p&gt;Expect around $5 per month at minimum for a basic VPS with reasonable bandwidth.&lt;/p&gt;
&lt;p&gt;You need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enough bandwidth for your traffic (1-2 TB/month covers most homelabs)&lt;/li&gt;
&lt;li&gt;Stable networking&lt;/li&gt;
&lt;li&gt;Latency to your home under 100 ms if possible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most homelabs, this is the cost of a fancy coffee once a month.&lt;/p&gt;
&lt;h3 id="added-latency"&gt;Added Latency
&lt;/h3&gt;&lt;p&gt;Traffic now takes a detour:&lt;/p&gt;
&lt;p&gt;User → VPS → Home → VPS → User&lt;/p&gt;
&lt;p&gt;That adds latency. How much depends on the distance between you and your VPS.&lt;/p&gt;
&lt;p&gt;For dashboards, usually negligible. For media streaming, typically acceptable because buffering absorbs the delay. For real-time gaming, possibly noticeable. If latency climbs above 200 ms, choose a VPS region closer to you. You can also tune WireGuard MTU (try 1280) if you suspect packet fragmentation is making things worse.&lt;/p&gt;
&lt;h3 id="you-must-secure-the-vps"&gt;You Must Secure the VPS
&lt;/h3&gt;&lt;p&gt;The VPS becomes your edge. Treat it like one. If you leave it misconfigured, you&amp;rsquo;ve only moved the problem from your house to a server you&amp;rsquo;re also responsible for.&lt;/p&gt;
&lt;p&gt;You should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use SSH keys only (disable password auth in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Configure a host firewall (&lt;code&gt;ufw&lt;/code&gt; or &lt;code&gt;nftables&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Keep the system updated (&lt;code&gt;unattended-upgrades&lt;/code&gt; on Debian/Ubuntu is your friend)&lt;/li&gt;
&lt;li&gt;Monitor logs (even a simple &lt;code&gt;logwatch&lt;/code&gt; cron job beats nothing)&lt;/li&gt;
&lt;li&gt;Run nothing else on this box if you can help it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A VPS introduces its own management and patching burden. But it&amp;rsquo;s far easier to defend a minimal single-purpose VPS than an entire lab network with dozens of services hanging off your residential IP.&lt;/p&gt;
&lt;h3 id="realistic-ddos-expectations"&gt;Realistic DDoS Expectations
&lt;/h3&gt;&lt;p&gt;A $5 VPS is not a bulletproof DDoS shield. Budget providers may null-route your IP if you attract significant attack traffic. That means your services go offline until the attack subsides.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s why it&amp;rsquo;s still better than direct exposure: the attack lands on the VPS. Your family&amp;rsquo;s internet keeps working. You redeploy the VPS with a new IP, update DNS, and move on. Compare that to someone DDoSing your home connection directly, where your only option is to call your ISP and wait on hold.&lt;/p&gt;
&lt;p&gt;The VPS shifts where the attack lands. It doesn&amp;rsquo;t eliminate DDoS risk entirely. Don&amp;rsquo;t kid yourself on that.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;&lt;br&gt;
A compact mini-workstation built around up to a 16-core Ryzen 9 9955HX, with dual 10GbE SFP+ plus dual 2.5GbE, flexible storage (U.2 + M.2 including 22110), and triple 8K display outputs. Great as a homelab node or small server with serious I/O.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="alternatives-compared"&gt;Alternatives Compared
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Home IP Hidden&lt;/th&gt;
&lt;th&gt;Protocol Support&lt;/th&gt;
&lt;th&gt;Management&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct Port Forwarding&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Temporary testing only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Tunnel / Zero Trust&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;HTTP/HTTPS mainly&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;td&gt;Low-traffic web apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale / WireGuard Only&lt;/td&gt;
&lt;td&gt;No (if publishing)&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;td&gt;Private remote access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPS + WireGuard&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;~$5/month&lt;/td&gt;
&lt;td&gt;Public-facing homelab services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Cloud Migration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;$$$&lt;/td&gt;
&lt;td&gt;Business-critical workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="direct-port-forwarding"&gt;Direct Port Forwarding
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Free, simple.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Exposes home IP, constant scanning, no DDoS buffer, high risk if misconfigured.&lt;br&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Temporary testing only. If you&amp;rsquo;re still using this in production, you&amp;rsquo;re living on borrowed time.&lt;/p&gt;
&lt;h3 id="cloudflare-tunnel--homelab-warp-or-zero-trust"&gt;Cloudflare Tunnel / Homelab Warp or Zero Trust
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: No open ports, easy setup, DDoS mitigation, good for web apps.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: TLS termination by a third party, vendor lock-in, bandwidth constraints for streaming, limited protocol flexibility.&lt;br&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Low-traffic HTTP apps, quick deployments.&lt;/p&gt;
&lt;h3 id="tailscale-or-direct-wireguard-only"&gt;Tailscale or Direct WireGuard Only
&lt;/h3&gt;&lt;p&gt;Using WireGuard or Tailscale without a VPS works well for internal access. It&amp;rsquo;s great for reaching your lab from your phone or laptop on the road.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Secure, no public exposure, excellent for private access.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Not designed for anonymous public users. If you publish services directly, your home IP is still exposed.&lt;br&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Personal remote access, admin connections.&lt;/p&gt;
&lt;h3 id="vps-reverse-proxy--wireguard"&gt;VPS Reverse Proxy + WireGuard
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Hides home IP, full control over TLS, works for HTTP and non-HTTP, flexible routing, reduced residential exposure.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Monthly cost, added latency, extra management layer.&lt;br&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Serious homelabbers running public-facing services, media servers, or custom apps.&lt;/p&gt;
&lt;h3 id="full-cloud-migration"&gt;Full Cloud Migration
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Enterprise-grade infrastructure, no home exposure.&lt;br&gt;
&lt;strong&gt;Cons&lt;/strong&gt;: Higher recurring costs, less homelab control, less fun.&lt;br&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Business-critical services, high-risk workloads, or when compliance requirements enter the picture.&lt;/p&gt;
&lt;h2 id="troubleshooting-and-operational-realities"&gt;Troubleshooting and Operational Realities
&lt;/h2&gt;&lt;p&gt;Even good solutions need maintenance. Here&amp;rsquo;s where things break.&lt;/p&gt;
&lt;h3 id="wireguard-tunnel-drops"&gt;WireGuard Tunnel Drops
&lt;/h3&gt;&lt;p&gt;Symptoms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Services intermittently unavailable&lt;/li&gt;
&lt;li&gt;Reverse proxy returns 502&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wg show&lt;/code&gt; shows no recent handshake (or a handshake timestamp from hours ago)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Common causes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Home IP changed (dynamic IP from your ISP)&lt;/li&gt;
&lt;li&gt;NAT timeout on your home router killed the session&lt;/li&gt;
&lt;li&gt;MTU mismatch causing packet fragmentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mitigations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; on the home peer&amp;rsquo;s WireGuard config. This sends a keepalive packet every 25 seconds, preventing NAT tables from expiring your session.&lt;/li&gt;
&lt;li&gt;Lower MTU to 1280 in your WireGuard interface config if fragmentation is suspected&lt;/li&gt;
&lt;li&gt;Monitor handshake timestamps with a cron job. Something like &lt;code&gt;wg show wg0 latest-handshakes&lt;/code&gt; piped into a simple alerting script works.&lt;/li&gt;
&lt;li&gt;Use a dynamic DNS updater (&lt;code&gt;ddclient&lt;/code&gt; or a provider&amp;rsquo;s API script) if your home IP changes frequently&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="reverse-proxy-errors"&gt;Reverse Proxy Errors
&lt;/h3&gt;&lt;p&gt;If the VPS returns 502 or 504:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirm the homelab service is actually running (&lt;code&gt;systemctl status &amp;lt;service&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Verify routing over the tunnel subnet (can the VPS &lt;code&gt;ping&lt;/code&gt; your homelab&amp;rsquo;s WireGuard IP?)&lt;/li&gt;
&lt;li&gt;Check SNI and TLS configuration in your reverse proxy&lt;/li&gt;
&lt;li&gt;Ensure firewall rules on both ends allow tunnel traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="unexpected-public-exposure"&gt;Unexpected Public Exposure
&lt;/h3&gt;&lt;p&gt;Run:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;nmap yourdomain.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You should only see the VPS ports.&lt;/p&gt;
&lt;p&gt;If you see your ISP IP in DNS records, HTTP headers, or service responses, investigate immediately. Check for DNS leaks (stale A records pointing home), misconfigured &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers, or services that embed your local IP in responses (some apps are chatty about this in their default configs).&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;&lt;br&gt;
A palm-sized single-board computer with a quad-core ARM CPU and 4GB RAM that runs full Linux, with Gigabit Ethernet, dual-band Wi-Fi, USB 3.0, and dual micro-HDMI. Ideal for lightweight homelab duties or for running a WireGuard endpoint while sipping a few watts.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How does a VPS hide my homelab IP from attacks?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;All public DNS records point to the VPS IP. Attackers reach the VPS and stop there. Your home IP is never published in DNS or exposed through HTTP headers. The WireGuard tunnel is outbound-initiated from your home, so no inbound firewall rules are needed on your router.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What happens if my VPS provider null-routes me during an attack?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Your public services go offline temporarily, but your home internet stays unaffected. You can redeploy the VPS with a new IP, update DNS, and be back online. This is far better than having your entire home connection taken down.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use WireGuard port forwarding without a VPS for public services?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;WireGuard alone is excellent for private access between your devices. For public-facing services without a VPS, you still need to forward ports on your home router, which exposes your residential IP. The VPS is what removes that exposure.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What about CGNAT? Does that change the equation?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;If your ISP uses CGNAT, you can&amp;rsquo;t port forward at all without their cooperation. A VPS with WireGuard actually solves this problem entirely. Your homelab connects outbound to the VPS, and the VPS handles all public traffic. CGNAT doesn&amp;rsquo;t matter because you&amp;rsquo;re never accepting inbound connections at home.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I handle a dynamic home IP with this setup?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Set up a dynamic DNS client (like &lt;code&gt;ddclient&lt;/code&gt; or your DNS provider&amp;rsquo;s update API) on your homelab that updates a hostname whenever your IP changes. Configure the WireGuard peer on the VPS side to use that hostname instead of a hardcoded IP. Combined with &lt;code&gt;PersistentKeepalive&lt;/code&gt;, the tunnel re-establishes automatically after an IP change.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the cheapest VPS that works for homelab proxying?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;Look for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Around $5 per month&lt;/li&gt;
&lt;li&gt;1 GB RAM&lt;/li&gt;
&lt;li&gt;1 vCPU&lt;/li&gt;
&lt;li&gt;Generous bandwidth (1-2 TB minimum)&lt;/li&gt;
&lt;li&gt;Static IPv4&lt;/li&gt;
&lt;li&gt;KVM virtualization (avoid OpenVZ for WireGuard kernel module support)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Latency to your home should ideally be under 100 ms.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ When should I stop self-hosting and move to the cloud?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;If you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handle sensitive client data&lt;/li&gt;
&lt;li&gt;Require guaranteed uptime with SLAs&lt;/li&gt;
&lt;li&gt;Face compliance requirements (HIPAA, SOC 2, etc.)&lt;/li&gt;
&lt;li&gt;Attract sustained, large-scale attack traffic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It may be time to move critical services fully to the cloud. A VPS plus homelab combo works great for personal projects and media, but it&amp;rsquo;s not a substitute for managed infrastructure when the stakes are high.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="lessons-learned-from-doing-it-wrong-first"&gt;Lessons Learned From Doing It Wrong First
&lt;/h2&gt;&lt;p&gt;I ran my homelab with a reverse proxy at home, Cloudflare in front, and strong passwords. I figured that was enough.&lt;/p&gt;
&lt;p&gt;It wasn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;My IP was still exposed in certain scenarios. My logs showed constant probing. I was handing TLS termination to a third party without fully considering what that meant. And my home network was still in the blast radius if anything went sideways.&lt;/p&gt;
&lt;p&gt;After moving to a VPS running Caddy as a reverse proxy with a WireGuard tunnel back home, I got IP privacy, cleaner architecture, zero exposed ports on my router, and a lot more confidence that a bad day on the internet wouldn&amp;rsquo;t become a bad day for my household.&lt;/p&gt;
&lt;p&gt;The setup isn&amp;rsquo;t complicated. It requires you to accept that &amp;ldquo;good enough&amp;rdquo; wasn&amp;rsquo;t good enough.&lt;/p&gt;
&lt;h2 id="the-right-architecture-for-serious-homelabs"&gt;The Right Architecture for Serious Homelabs
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re casually experimenting, port forwarding might feel fine for now. Know what you&amp;rsquo;re accepting.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re serious about running public-facing services, a VPS in front is the better way to keep your home network out of the blast radius. It hides your home IP, eliminates router port forwarding, gives you full TLS control, works beyond HTTP, and scales with you.&lt;/p&gt;
&lt;p&gt;Cloudflare and zero trust tools are useful. Tailscale is fantastic for private access. But if you want full control, privacy, and protocol flexibility, the VPS plus WireGuard model is the sweet spot for anyone looking to self-host their own edge.&lt;/p&gt;
&lt;p&gt;Your homelab should be a playground, not a liability. If you&amp;rsquo;re going to open it to the internet, do it the right way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is Part 1 of a 3-part series on building a VPS-fronted homelab.&lt;/strong&gt; Once you understand why a VPS edge beats home port forwarding, the next two parts walk through building it:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/featured_hu_de8a9ae15c68c1b0.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/featured_hu_c13d56a28e255655.webp" width="100" height="100" alt="How to Install WireGuard on a VPS and Connect It to Your Homelab" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 2: Install WireGuard on a VPS&lt;/strong&gt;
Build the VPS-to-homelab WireGuard tunnel with iptables port forwarding and a Caddy reverse proxy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/install-wireguard-vps-homelab-tunnel/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 2
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/featured_hu_967326aab8997f75.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/featured_hu_ea64966e411a3d5b.webp" width="100" height="100" alt="WireGuard Client on OPNsense and pfSense: LAN Routing for Your VPS Tunnel" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Part 3: Route the Tunnel to Your LAN&lt;/strong&gt;
Set up the WireGuard client on OPNsense or pfSense so every device on your network reaches the tunnel.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read Part 3
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Part 3: &lt;a class="link" href="https://diymediaserver.com/post/2026/wireguard-client-opnsense-pfsense-lan-routing/" &gt;WireGuard Client on OPNsense and pfSense: LAN Routing for Your VPS Tunnel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>How to Fix Stale File Handles NFS Errors with MergerFS</title><link>https://diymediaserver.com/post/2026/fix-stale-nfs-file-handles-mergerfs/</link><pubDate>Sat, 14 Feb 2026 05:26:30 -0700</pubDate><guid>https://diymediaserver.com/post/2026/fix-stale-nfs-file-handles-mergerfs/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/fix-stale-nfs-file-handles-mergerfs/featured_hu_d9955e767fb26f40.webp" alt="Featured image of post How to Fix Stale File Handles NFS Errors with MergerFS " /&gt;&lt;p&gt;If you&amp;rsquo;re running a homelab NAS with a MergerFS and SnapRAID setup and sharing that pool over NFS, there&amp;rsquo;s a good chance you&amp;rsquo;ve hit one of the most frustrating errors in self-hosting: stale file handles. Files that were accessible five minutes ago suddenly aren&amp;rsquo;t. Containers break. Plex stops mid-stream. You restart everything, it works for a while, and then it happens again.&lt;/p&gt;
&lt;p&gt;I spent way too long chasing stale file handles on NFS before landing on the real cause. It turned out to be a fundamental mismatch between how MergerFS works as a FUSE filesystem and what NFS expects from its exports. This post walks through the problem, why it happens, and the fix that finally made it go away for good.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
&lt;br&gt;
Sharing a MergerFS pool directly over NFS causes stale file handles because NFS can't reliably track file handles across a FUSE filesystem. The fix is to export each underlying disk individually over NFS, then install MergerFS on the client machine and pool them there. Your SnapRAID config on the NAS doesn't need to change at all since it already points at the individual drives. The stale handle errors go away permanently because NFS is finally working with real filesystems instead of a FUSE layer.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-mergerfs-and-nfs-cause-stale-file-handles"&gt;Why MergerFS and NFS Cause Stale File Handles
&lt;/h2&gt;&lt;p&gt;MergerFS is a FUSE-based union filesystem. It takes multiple individual drives and presents them as a single merged mount point. If you&amp;rsquo;re running a typical MergerFS and SnapRAID homelab stack, you probably have several data disks pooled together with MergerFS for day-to-day use and SnapRAID providing parity protection on top. That architecture is great for local use. Your applications see one big pool and MergerFS handles file placement across the underlying disks based on your chosen policy.&lt;/p&gt;
&lt;p&gt;The trouble starts when you export that merged FUSE mount point directly over NFS. NFS relies on file handles to track files across the network. These handles are tied to the underlying filesystem&amp;rsquo;s inode and device information. Because MergerFS runs as a FUSE filesystem in userspace rather than as a native kernel filesystem, it generates its own virtual file handles. When MergerFS resolves paths across drives, or when SnapRAID maintenance operations like syncs and scrubs touch the underlying files, those FUSE-generated handles can become invalid from NFS&amp;rsquo;s perspective. The NFS client asks for a file using a handle that no longer points to anything valid, and you get the dreaded &lt;code&gt;Stale file handle&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a bug or a misconfiguration you can tune away.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/mergerfs-logo_hu_2c4b199c032060d0.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/mergerfs-logo_hu_eda5df3f7ab38ce2.webp" width="100" height="100" alt="MergerFS official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;MergerFS official Documentation&lt;/strong&gt;&lt;br&gt;
Explicitly calls this out. Exporting a FUSE-based MergerFS pool directly over NFS is not a supported workflow.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://trapexit.github.io/mergerfs/latest/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-fix-export-individual-drives-over-nfs-run-mergerfs-on-the-client"&gt;The Fix: Export Individual Drives Over NFS, Run MergerFS on the Client
&lt;/h2&gt;&lt;p&gt;The solution is to flip the architecture. Instead of pooling on the server and exporting the MergerFS mount, you export each underlying drive individually over NFS and then run MergerFS on the client side to combine them back into a single merged view.&lt;/p&gt;
&lt;p&gt;It sounds like more work, but the setup is straightforward and the result is rock solid. NFS gets stable, predictable file handles because each export is a real ext4 or XFS filesystem on a real disk, not a virtual FUSE layer. MergerFS still gives you a single unified pool. It runs on the machine that actually consumes the data. And your SnapRAID parity setup on the NAS stays completely untouched, since SnapRAID operates on the individual drives rather than the merged pool.&lt;/p&gt;
&lt;h3 id="step-1-export-each-drive-individually-from-the-nas"&gt;Step 1: Export Each Drive Individually from the NAS
&lt;/h3&gt;&lt;p&gt;On your NAS (the NFS server), edit &lt;code&gt;/etc/exports&lt;/code&gt; to export each disk in your MergerFS pool as its own NFS share. The critical detail here is that each export needs a unique &lt;code&gt;fsid&lt;/code&gt; value. NFS uses &lt;code&gt;fsid&lt;/code&gt; to distinguish between exports, and if you skip this or duplicate values, you&amp;rsquo;ll get weird cross-mount issues that look a lot like the stale file handle problem you&amp;rsquo;re trying to fix.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/mnt/Pool/Disk1/ 192.168.1.0/24(all_squash,anongid=1001,anonuid=1000,insecure,rw,subtree_check,fsid=1)
/mnt/Pool/Disk2/ 192.168.1.0/24(all_squash,anongid=1001,anonuid=1000,insecure,rw,subtree_check,fsid=2)
/mnt/Pool/Disk3/ 192.168.1.0/24(all_squash,anongid=1001,anonuid=1000,insecure,rw,subtree_check,fsid=3)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Repeat this pattern for every data disk in your pool. If you&amp;rsquo;re running SnapRAID, these are your data disks. You don&amp;rsquo;t need to export the SnapRAID parity disk. A few things to double-check on the export options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;anonuid&lt;/code&gt; and &lt;code&gt;anongid&lt;/code&gt;&lt;/strong&gt; should match the user and group IDs that own your media files (or whatever you&amp;rsquo;re serving). Run &lt;code&gt;id &amp;lt;username&amp;gt;&lt;/code&gt; on the NAS if you&amp;rsquo;re not sure what these should be. Getting this wrong leads to permission denied errors that are easy to confuse with stale NFS file handles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;all_squash&lt;/code&gt;&lt;/strong&gt; maps all client requests to the anonymous user, which keeps permissions simple in a homelab. If you need per-user access control, you&amp;rsquo;ll want a different approach, but for most media server and homelab setups this is the right call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;insecure&lt;/code&gt;&lt;/strong&gt; allows connections from ports above 1024. Some NFS clients (especially on non-Linux systems or inside containers) need this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The subnet&lt;/strong&gt; (&lt;code&gt;192.168.1.0/24&lt;/code&gt; in this example) should match your actual network. Restrict this to the range that needs access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After editing, apply the changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo exportfs -ra
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm the exports are live:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo exportfs -v
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-2-mount-the-individual-nfs-shares-on-the-client"&gt;Step 2: Mount the Individual NFS Shares on the Client
&lt;/h3&gt;&lt;p&gt;On each client machine that needs access to the pool, start by creating the local mount points:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/pool/disk1 /mnt/pool/disk2 /mnt/pool/disk3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then add the NFS mounts to &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;192.168.1.5:/mnt/Pool/Disk1 /mnt/pool/disk1 nfs rw,nofail,hard,intr 0 0
192.168.1.5:/mnt/Pool/Disk2 /mnt/pool/disk2 nfs rw,nofail,hard,intr 0 0
192.168.1.5:/mnt/Pool/Disk3 /mnt/pool/disk3 nfs rw,nofail,hard,intr 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;192.168.1.5&lt;/code&gt; with the actual IP of your NAS. The mount options here are worth understanding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hard&lt;/code&gt;&lt;/strong&gt; means the client will keep retrying if the NFS server becomes unreachable, rather than returning an error. For a homelab where reboots happen, this is usually what you want. The alternative, &lt;code&gt;soft&lt;/code&gt;, gives up after a timeout, which can lead to data corruption if a write was in progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;intr&lt;/code&gt;&lt;/strong&gt; allows you to interrupt a hung NFS operation with a signal. Without this, a stuck NFS mount can lock up processes in a way that&amp;rsquo;s hard to recover from without a reboot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nofail&lt;/code&gt;&lt;/strong&gt; prevents the client from hanging at boot if the NAS isn&amp;rsquo;t available yet. Especially important if your NAS and clients boot at the same time after a power outage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mount everything:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify each disk is accessible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /mnt/pool/disk1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /mnt/pool/disk2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /mnt/pool/disk3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the individual NFS mounts are stable, the next piece is the network link they all ride on. With three or four NFS shares running side by side, a 1GbE port becomes the bottleneck fast.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;&lt;br&gt;
Mounting three or four NFS shares on the client and pooling them through a MergerFS FUSE mount means your network link matters more than ever. This 2.5GbE PCIe card is cheap, works out of the box on most Linux distros, and makes sure the NIC isn&amp;rsquo;t the bottleneck when you&amp;rsquo;re streaming from a client-side merged pool.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="step-3-create-the-mergerfs-fuse-pool-on-the-client"&gt;Step 3: Create the MergerFS FUSE Pool on the Client
&lt;/h3&gt;&lt;p&gt;Install MergerFS on the client if you haven&amp;rsquo;t already. On Debian/Ubuntu, grab the latest .deb from the MergerFS releases page:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/trapexit/mergerfs/releases" target="_blank" rel="noopener"
&gt;https://github.com/trapexit/mergerfs/releases&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo dpkg -i mergerfs_&amp;lt;version&amp;gt;.deb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the directory where the merged FUSE pool will appear:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then add the MergerFS FUSE mount to &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/mnt/pool/disk* /media/storage fuse.mergerfs direct_io,defaults,allow_other,dropcacheonclose=true,inodecalc=path-hash,category.create=mfs,minfreespace=50G,fsname=storage 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s what the key MergerFS options are doing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;direct_io&lt;/code&gt;&lt;/strong&gt; - Bypasses the kernel page cache. This is important when the underlying filesystems are NFS mounts, since the NFS client has its own caching layer. Double-caching through both NFS and FUSE leads to stale reads and wasted memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;dropcacheonclose=true&lt;/code&gt;&lt;/strong&gt; - Drops cached data when a file is closed. Another safeguard against stale data when MergerFS operates over NFS-mounted filesystems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;inodecalc=path-hash&lt;/code&gt;&lt;/strong&gt; - Generates inode numbers based on the file path rather than the underlying device. This keeps inode numbers stable even if files exist on different disks, which matters for applications that track files by inode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;category.create=mfs&lt;/code&gt;&lt;/strong&gt; - The &amp;ldquo;most free space&amp;rdquo; policy. New files get written to whichever disk has the most available space. Good default for media storage. If you&amp;rsquo;re using SnapRAID, keep in mind that new files written through this policy won&amp;rsquo;t be protected until the next &lt;code&gt;snapraid sync&lt;/code&gt; runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;minfreespace=50G&lt;/code&gt;&lt;/strong&gt; - Disks with less than 50GB free won&amp;rsquo;t receive new files. Adjust this based on your disk sizes. You don&amp;rsquo;t want a drive filling up to 100%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;allow_other&lt;/code&gt;&lt;/strong&gt; - Lets users other than the one who mounted the FUSE filesystem access it. Required if you&amp;rsquo;re running services like Plex, Jellyfin, or containerized apps under different user accounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mount it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check that the merged pool looks right:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see a single view of all your files across all disks, the same as before, but now without the stale file handle errors. Fewer, larger disks per NAS also means fewer NFS exports to manage and a simpler fstab on every client.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;br&gt;
When each disk in your MergerFS pool gets its own NFS export, bigger drives mean fewer exports to manage and fewer fstab entries on each client. 24TB per disk also means your SnapRAID parity drive covers a lot of storage per slot. Fewer disks, less complexity, same merged pool on the other end.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-this-fixes-stale-nfs-file-handles"&gt;Why This Fixes Stale NFS File Handles
&lt;/h2&gt;&lt;p&gt;The root cause was always about how FUSE filesystems interact with NFS file handles. When NFS exports a real, single-disk filesystem like ext4 or XFS, the file handles are based on stable inode and device IDs that don&amp;rsquo;t change. The NFS server and client stay in agreement about what each handle points to.&lt;/p&gt;
&lt;p&gt;MergerFS, as a FUSE-based filesystem running in userspace, generates its own virtual file handles that can shift when the underlying layout changes. NFS has no mechanism to track those shifts across a FUSE layer, so the handles go stale. This is a fundamental limitation of exporting any FUSE filesystem over NFS, and it isn&amp;rsquo;t unique to MergerFS.&lt;/p&gt;
&lt;p&gt;By moving the MergerFS FUSE mount to the client side, NFS only ever deals with real on-disk filesystems. Each NFS export is a direct, one-to-one mapping to a physical disk partition. The file handles stay valid indefinitely. MergerFS then operates entirely in local userspace on the client, where FUSE handle behavior doesn&amp;rsquo;t need to survive a network round-trip.&lt;/p&gt;
&lt;h2 id="what-happens-to-snapraid-in-this-setup"&gt;What Happens to SnapRAID in This Setup
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re running SnapRAID alongside MergerFS (which is one of the most common storage configurations in homelabs), nothing changes on the server side. SnapRAID always operates on the individual data disks, not on the MergerFS pool. Your SnapRAID configuration file still points to the same &lt;code&gt;/mnt/Pool/Disk1&lt;/code&gt;, &lt;code&gt;/mnt/Pool/Disk2&lt;/code&gt;, etc. paths. Your parity disk stays local to the NAS. Your &lt;code&gt;snapraid sync&lt;/code&gt; and &lt;code&gt;snapraid scrub&lt;/code&gt; cron jobs keep running exactly as before.&lt;/p&gt;
&lt;p&gt;The only thing that changed is how those disks get to the client machines. Instead of NFS exporting a single FUSE-merged path, you&amp;rsquo;re exporting the same underlying directories that SnapRAID already knows about. If anything, this architecture is cleaner because there&amp;rsquo;s no ambiguity about which layer owns what. SnapRAID and NFS both work directly with the real filesystems, and MergerFS handles the convenience of a unified view on whatever machine needs it.&lt;/p&gt;
&lt;h2 id="things-to-watch-out-for"&gt;Things to Watch Out For
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Boot order matters.&lt;/strong&gt;&lt;br&gt;
The NFS mounts need to be up before the MergerFS FUSE mount tries to pool them. In most cases, &lt;code&gt;systemd&lt;/code&gt; handles this correctly because the fstab entries are processed in order and &lt;code&gt;fuse.mergerfs&lt;/code&gt; depends on the mount points being available. If you&amp;rsquo;re seeing empty pools after a reboot, look into adding &lt;code&gt;x-systemd.requires&lt;/code&gt; or &lt;code&gt;x-systemd.after&lt;/code&gt; options to the MergerFS fstab line, or use an automount approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adding new disks.&lt;/strong&gt;&lt;br&gt;
When you add a new drive to the NAS, you need to add a new NFS export on the server, a new NFS mount entry on each client, and update the MergerFS glob pattern if the new disk doesn&amp;rsquo;t match &lt;code&gt;/mnt/pool/disk*&lt;/code&gt;. If you&amp;rsquo;re running SnapRAID, you&amp;rsquo;ll also need to add the new disk to your SnapRAID config and run a sync. You&amp;rsquo;d have to do that regardless of how you export over NFS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SnapRAID sync timing.&lt;/strong&gt;&lt;br&gt;
Since new files written through the client-side MergerFS pool end up on individual disks on the NAS, they won&amp;rsquo;t have parity protection until the next &lt;code&gt;snapraid sync&lt;/code&gt;. This is the same as any MergerFS and SnapRAID setup. It isn&amp;rsquo;t introduced by this architecture change. If you&amp;rsquo;re not already running SnapRAID syncs on a schedule, set up a cron job or use a helper script like &lt;a class="link" href="https://github.com/Chronial/snapraid-runner" target="_blank" rel="noopener"
&gt;snapraid-runner&lt;/a&gt; to automate it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt;&lt;br&gt;
Running a FUSE filesystem on the client over NFS mounts adds a small layer of overhead compared to a direct NFS export. In practice, for media streaming, file serving, and typical homelab workloads, this is negligible. The FUSE overhead is mostly in metadata operations, and the NFS network latency dominates actual file transfer times anyway. If you&amp;rsquo;re doing heavy random I/O or database-style workloads over this setup, you might want to benchmark, but that&amp;rsquo;s probably not what you&amp;rsquo;re using MergerFS for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple clients.&lt;/strong&gt;&lt;br&gt;
Each client that needs the merged view needs its own MergerFS install and FUSE mount configuration. This is the main tradeoff. It&amp;rsquo;s per-client setup instead of a single pool on the NAS. For most homelabs with one or two client machines, it&amp;rsquo;s not a big deal. If you have many clients, you might want to script the deployment or use configuration management.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;Stale file handles on NFS with a MergerFS FUSE pool is one of those problems where the symptoms point you in every direction except the actual cause. You&amp;rsquo;ll check NFS timeouts, restart services, fiddle with cache settings, and none of it sticks. The real fix is architectural. Stop exporting the MergerFS FUSE mount over NFS. Export the individual drives directly and pool them with MergerFS on whatever client needs the merged view.&lt;/p&gt;
&lt;p&gt;Your SnapRAID parity stays exactly where it is. Your NFS file handles stay stable because they&amp;rsquo;re backed by real filesystems instead of a FUSE layer. And you stop re-troubleshooting the same stale file handle errors every few weeks, which honestly is the best part.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re sizing a new NAS around this architecture, a small bay count with a capable CPU is the sweet spot. You&amp;rsquo;re now serving multiple NFS exports per pool, and the pooling work has moved off the server entirely.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0G1C1YNN8"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ugreen-NAS_hu_9b2b8c726e38b8a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ugreen-NAS_hu_7dd7553917d2893c.webp" width="600" height="353" alt="UGREEN NASync DXP4800 Pro 4-Bay Desktop NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;UGREEN NASync DXP4800 Pro 4-Bay Desktop NAS&lt;/strong&gt;&lt;br&gt;
Four bays, an Intel Core i3-1315U, and dual networking (10GbE + 2.5GbE). Four bays is the sweet spot for a MergerFS and SnapRAID setup. Three data disks pooled with MergerFS and one dedicated SnapRAID parity drive. Plenty of CPU headroom to serve multiple NFS exports without the FUSE overhead since pooling happens on the client side.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4cmMHVT" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How to Fix Proxmox Status 30 Errors with Unprivileged LXCs</title><link>https://diymediaserver.com/post/2026/fix-proxmox-status-30-errors/</link><pubDate>Sat, 07 Feb 2026 07:18:30 -0700</pubDate><guid>https://diymediaserver.com/post/2026/fix-proxmox-status-30-errors/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/fix-proxmox-status-30-errors/featured_hu_5414a41414b96f0d.webp" alt="Featured image of post How to Fix Proxmox Status 30 Errors with Unprivileged LXCs" /&gt;&lt;p&gt;You upgraded to Proxmox 9.1.5 and now your Jellyfin, Plex, or Arr containers refuse to start. They worked for months, maybe years, and now all you get is a cryptic &amp;ldquo;Status 30&amp;rdquo; error. Proxmox 9.1.5 enforces &lt;code&gt;idmapped&lt;/code&gt; mounts for unprivileged LXC containers, and FUSE-based filesystems like MergerFS want nothing to do with them. My best guess is that these changes were made to better support the new OCI-compliant images, which let Proxmox run Docker images without needing Docker installed.&lt;/p&gt;
&lt;p&gt;The fix: ditch &lt;code&gt;mpX&lt;/code&gt; mounts entirely, replace them with &lt;code&gt;lxc.mount.entry&lt;/code&gt; lines, and configure explicit unprivileged ID mapping.&lt;/p&gt;
&lt;p&gt;This guide is for Proxmox users running unprivileged LXC containers with bind mounts on MergerFS or NFS storage. If all your storage is native Proxmox (ZFS, ext4, local directories), your &lt;code&gt;mpX&lt;/code&gt; mounts will probably still work. You can stop reading and go enjoy your day.&lt;/p&gt;
&lt;p&gt;I hit this after updating to Proxmox 9.1.5. Every container with MergerFS bind mounts failed after a reboot. No useful error message. No graceful fallback. Only &amp;ldquo;Status 30&amp;rdquo; and silence. After digging through logs and testing half a dozen approaches, the solution turned out to be straightforward. Annoying, but straightforward.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Proxmox 9.1.5 enforces idmapped mounts for &lt;code&gt;mpX&lt;/code&gt; bind mounts. MergerFS and most NFS setups don&amp;rsquo;t support idmapped mounts, which causes LXC startup to fail with Status 30. The fix: remove your &lt;code&gt;mpX&lt;/code&gt; mounts, use &lt;code&gt;lxc.mount.entry&lt;/code&gt; instead, and explicitly define your unprivileged UID and GID mappings. Details below.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="quick-check-does-this-apply-to-you"&gt;Quick Check: Does This Apply to You?
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Your storage is probably fine if it&amp;rsquo;s:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Local ext4, XFS, or ZFS on the Proxmox host&lt;/li&gt;
&lt;li&gt;Native Proxmox directories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;You&amp;rsquo;re affected if your storage is:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MergerFS pools&lt;/li&gt;
&lt;li&gt;NFS mounts&lt;/li&gt;
&lt;li&gt;Other FUSE-based filesystems&lt;/li&gt;
&lt;li&gt;External USB or hybrid storage setups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re in the second group, keep reading. Everyone else, go do something more fun.&lt;/p&gt;
&lt;h2 id="what-is-the-proxmox-status-30-error"&gt;What Is the Proxmox Status 30 Error?
&lt;/h2&gt;&lt;p&gt;Status 30 is LXC&amp;rsquo;s way of telling you something went wrong during container startup without telling you &lt;em&gt;what&lt;/em&gt; went wrong. It&amp;rsquo;s a generic failure code that masks deeper filesystem or permission problems.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll see messages like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;startup for container '102' failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Script exited with status 30&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Read-only file system (os error 30)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
Status 30 means Proxmox tried to mount or write to storage and the kernel said no.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;To find out &lt;em&gt;why&lt;/em&gt; the kernel said no, you need to watch the errors live. Run this on the Proxmox host, replacing &lt;code&gt;XXX&lt;/code&gt; with your container ID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc-start -n XXX -F -l DEBUG -o /dev/stdout
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for lines mentioning &lt;code&gt;idmapped&lt;/code&gt;, &lt;code&gt;mount&lt;/code&gt;, or &lt;code&gt;permission denied&lt;/code&gt;. Those will point you toward the actual cause.&lt;/p&gt;
&lt;h2 id="what-changed-in-proxmox-915"&gt;What Changed in Proxmox 9.1.5?
&lt;/h2&gt;&lt;p&gt;Proxmox 9.1.5 ships with a newer Linux kernel and tighter security defaults. The specific change that breaks MergerFS setups is broader enforcement of idmapped mounts for unprivileged containers.&lt;/p&gt;
&lt;p&gt;When you define a container mount like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mp0: /mnt/media,mp=/media
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Proxmox now automatically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creates an idmapped mount&lt;/li&gt;
&lt;li&gt;Shifts ownership so container UID 1000 maps cleanly to host UID 1000&lt;/li&gt;
&lt;li&gt;Enforces isolation guarantees&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This works great on native filesystems (ext4, XFS, ZFS). It completely fails on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MergerFS&lt;/strong&gt; (FUSE-based, no idmap support)&lt;/li&gt;
&lt;li&gt;Most &lt;strong&gt;NFS exports&lt;/strong&gt; (server-side ownership, no client idmap)&lt;/li&gt;
&lt;li&gt;Some custom or layered storage stacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the kernel refuses the idmapped mount, Proxmox kills the LXC startup rather than opening a security hole. This shows up as a Status 30 error.&lt;/p&gt;
&lt;h2 id="the-mpx-trap-why-old-configs-break"&gt;The mpX Trap: Why Old Configs Break
&lt;/h2&gt;&lt;p&gt;Before Proxmox 9.1.5, &lt;code&gt;mpX&lt;/code&gt; mounts were forgiving. You could bind-mount media directories owned by UID 1000, and LXC handled permission translation loosely. Nobody complained.&lt;/p&gt;
&lt;p&gt;After the upgrade, &lt;code&gt;mpX&lt;/code&gt; mounts trigger the idmapping hook automatically. If your underlying filesystem doesn&amp;rsquo;t support it, three things happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container fails to start&lt;/li&gt;
&lt;li&gt;Misleading &amp;ldquo;read-only filesystem&amp;rdquo; errors appear in logs&lt;/li&gt;
&lt;li&gt;Ghost mounts get left behind on the host (more on this in Step 5)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why containers &lt;em&gt;without&lt;/em&gt; bind mounts start fine while your media servers all failed at the same time. Ask me how I know.&lt;/p&gt;
&lt;h2 id="the-fix-manual-mounting-and-explicit-unprivileged-id-mapping"&gt;The Fix: Manual Mounting and Explicit Unprivileged ID Mapping
&lt;/h2&gt;&lt;p&gt;The temptation here is to flip your containers to privileged mode. Don&amp;rsquo;t. Privileged containers run as root on the host. If your Jellyfin or Plex container gets exploited, the attacker has root on your entire Proxmox host and can pivot to every other VM and container you&amp;rsquo;re running. That&amp;rsquo;s not a theoretical risk. Container escape vulnerabilities in shared-kernel setups are well-documented, and if you&amp;rsquo;re running services that download files from the internet (every Arr app, every torrent client), you&amp;rsquo;re handing a potential attacker the keys to your entire homelab. Keep your containers unprivileged.&lt;/p&gt;
&lt;p&gt;The fix has three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Remove &lt;code&gt;mpX&lt;/code&gt; mounts so Proxmox stops trying to idmap them&lt;/li&gt;
&lt;li&gt;Use raw &lt;code&gt;lxc.mount.entry&lt;/code&gt; lines that LXC handles directly&lt;/li&gt;
&lt;li&gt;Define clean, contiguous UID and GID maps so permissions work correctly&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This keeps your containers unprivileged while sidestepping the MergerFS/NFS incompatibility entirely.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites checklist.&lt;/strong&gt; Before starting, confirm you have the following ready. Experienced readers: don&amp;rsquo;t skip Steps 1 and 2. The ID delegation and mpX removal on the host are required before anything else works.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH or console access to your Proxmox host (not the container)&lt;/li&gt;
&lt;li&gt;Your media user&amp;rsquo;s UID and GID (run &lt;code&gt;id your_media_username&lt;/code&gt; on the host)&lt;/li&gt;
&lt;li&gt;Your render group&amp;rsquo;s GID if you use GPU passthrough (run &lt;code&gt;getent group render&lt;/code&gt; on the host)&lt;/li&gt;
&lt;li&gt;The container ID(s) you need to fix&lt;/li&gt;
&lt;li&gt;A backup of your container config file(s)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-1-configure-uid-and-gid-delegation-on-the-host"&gt;Step 1: Configure UID and GID Delegation on the Host
&lt;/h2&gt;&lt;p&gt;Before LXC can map host IDs into an unprivileged container, Proxmox needs explicit permission to use those IDs. That permission lives in two files on the host: &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, figure out your actual media user&amp;rsquo;s UID and GID. Run this on the host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id your_media_username
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The examples below assume UID 1000 and GID 1001. &lt;strong&gt;Replace these with your actual values throughout.&lt;/strong&gt; If you copy-paste blindly and your IDs don&amp;rsquo;t match, you&amp;rsquo;ll be right back at Status 30. I won&amp;rsquo;t feel sorry for you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next&lt;/strong&gt;, check your render group GID if you need GPU passthrough:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getent group render
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This usually returns &lt;code&gt;render:x:104&lt;/code&gt; but it&amp;rsquo;s not guaranteed. On some distributions or custom setups, the render group could be GID 105, 128, or something else entirely. Whatever number you see here is the one you&amp;rsquo;ll use in every step below. If the command returns nothing, you don&amp;rsquo;t have a render group and can skip the render-related lines in Steps 1 and 4.&lt;/p&gt;
&lt;p&gt;Edit these files on the Proxmox host.&lt;/p&gt;
&lt;p&gt;Start with subuid:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/subuid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;root:1000:1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;root:100000:65536&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, edit subgid:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;root:104:1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;root:1001:1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;root:100000:65536&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;What each line means:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root:1000:1&lt;/code&gt; allows root to delegate host UID 1000 (your media user) into a container&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root:104:1&lt;/code&gt; allows root to delegate host GID 104 (the &lt;code&gt;render&lt;/code&gt; group, for GPU passthrough). Replace &lt;code&gt;104&lt;/code&gt; with whatever &lt;code&gt;getent group render&lt;/code&gt; returned on your system.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root:1001:1&lt;/code&gt; allows root to delegate host GID 1001 (your media group)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root:100000:65536&lt;/code&gt; is the standard unprivileged ID range. Leave it alone.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of these entries are missing or wrong, LXC cannot construct a valid idmap and your container won&amp;rsquo;t start. There&amp;rsquo;s no partial credit here.&lt;/p&gt;
&lt;h2 id="step-2-remove-mpx-mounts-from-the-container"&gt;Step 2: Remove mpX Mounts from the Container
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Back up your container config first.&lt;/strong&gt; I mean it. Copy it somewhere safe before you touch anything.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /etc/pve/lxc/XXX.conf /root/XXX.conf.bak
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now open the config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/pve/lxc/XXX.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Delete or comment out any lines like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;mp0: /mnt/media,mp=/media
mp1: /mnt/downloads,mp=/downloads
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Every single one. Leaving even one &lt;code&gt;mpX&lt;/code&gt; entry triggers the idmapping hook, and you&amp;rsquo;re back to Status 30.&lt;/p&gt;
&lt;h2 id="step-3-add-direct-lxcmountentry-bind-mounts"&gt;Step 3: Add Direct lxc.mount.entry Bind Mounts
&lt;/h2&gt;&lt;p&gt;At the bottom of the container config file, add manual mount entries:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.mount.entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/media/storage/Movies media/Movies none bind,create=dir,ro 0 0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.mount.entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/media/storage/Shows media/Shows none bind,create=dir,ro 0 0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pay attention to these details. They&amp;rsquo;ll bite you if you get them wrong:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;container path&lt;/strong&gt; (second field) is &lt;strong&gt;relative&lt;/strong&gt;, no leading slash. Write &lt;code&gt;media/Movies&lt;/code&gt;, not &lt;code&gt;/media/Movies&lt;/code&gt;. This trips people up constantly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bind&lt;/code&gt; tells LXC to handle the mount directly, bypassing Proxmox&amp;rsquo;s storage helpers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create=dir&lt;/code&gt; tells LXC to create the mount point inside the container if it doesn&amp;rsquo;t exist. Without this, you&amp;rsquo;ll get a mount failure if the directory is missing.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;ro&lt;/code&gt; or &lt;code&gt;rw&lt;/code&gt; explicitly based on what the container needs. Read-only where possible. Your Jellyfin container doesn&amp;rsquo;t need write access to your movie library.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These mounts are handled by LXC itself, not Proxmox. That&amp;rsquo;s the whole point.&lt;/p&gt;
&lt;h2 id="step-4-define-clean-unprivileged-id-mapping"&gt;Step 4: Define Clean Unprivileged ID Mapping
&lt;/h2&gt;&lt;p&gt;This is the part where most people get tripped up. Unprivileged containers require a fully contiguous UID and GID map covering the range 0 through 65535. No gaps. No overlaps. If the math doesn&amp;rsquo;t add up to exactly 65536 IDs, the container will not start. LXC is merciless about this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customize these values.&lt;/strong&gt; Replace &lt;code&gt;1000&lt;/code&gt; with your actual media user&amp;rsquo;s UID. Replace &lt;code&gt;1001&lt;/code&gt; with your actual media group&amp;rsquo;s GID. Replace &lt;code&gt;104&lt;/code&gt; with your actual render group GID.&lt;/p&gt;
&lt;h3 id="how-the-math-works"&gt;How the math works
&lt;/h3&gt;&lt;p&gt;Every &lt;code&gt;lxc.idmap&lt;/code&gt; line maps a range of container IDs to host IDs. The format is:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc.idmap: &amp;lt;u|g&amp;gt; &amp;lt;container_start&amp;gt; &amp;lt;host_start&amp;gt; &amp;lt;count&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The rule is simple: every &lt;code&gt;count&lt;/code&gt; value across all your lines for a given type (u or g) must add up to exactly 65536. When you pass through a specific ID (like your media UID), you split the range around it. Here&amp;rsquo;s the formula:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Range before passthrough: count = passthrough_id
Passthrough itself: count = 1
Range after passthrough: count = 65536 - passthrough_id - 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For multiple passthroughs (like the GID map), you split around each one in order, and the counts between them fill the gaps.&lt;/p&gt;
&lt;h3 id="example-a-uid-1000-gid-1001-render-gid-104"&gt;Example A: UID 1000, GID 1001, render GID 104
&lt;/h3&gt;&lt;p&gt;This is the most common homelab setup. Add these lines to your container config:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UID Mapping:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 0 100000 1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 1000 1000 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 1001 101001 64535&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container UIDs&lt;/th&gt;
&lt;th&gt;Host UIDs&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 - 999&lt;/td&gt;
&lt;td&gt;100000 - 100999&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;Unprivileged range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Media user passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001 - 65535&lt;/td&gt;
&lt;td&gt;101001 - 165535&lt;/td&gt;
&lt;td&gt;64535&lt;/td&gt;
&lt;td&gt;Unprivileged range&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;strong&gt;65536&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;GID Mapping:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 0 100000 104&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 104 104 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 105 100105 896&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1001 1001 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1002 101002 64534&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container GIDs&lt;/th&gt;
&lt;th&gt;Host GIDs&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 - 103&lt;/td&gt;
&lt;td&gt;100000 - 100103&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;Unprivileged range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Render group passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;105 - 1000&lt;/td&gt;
&lt;td&gt;100105 - 101000&lt;/td&gt;
&lt;td&gt;896&lt;/td&gt;
&lt;td&gt;Unprivileged range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Media group passthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1002 - 65535&lt;/td&gt;
&lt;td&gt;101002 - 165535&lt;/td&gt;
&lt;td&gt;64534&lt;/td&gt;
&lt;td&gt;Unprivileged range&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;strong&gt;65536&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Verify: 104 + 1 + 896 + 1 + 64534 = 65536. Contiguous. No gaps.&lt;/p&gt;
&lt;h3 id="example-b-uid-1001-gid-1002-render-gid-128"&gt;Example B: UID 1001, GID 1002, render GID 128
&lt;/h3&gt;&lt;p&gt;If your media user is UID 1001 with GID 1002, and your render group is GID 128, the math shifts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UID Mapping:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 0 100000 1001&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 1001 1001 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 1002 101002 64534&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify: 1001 + 1 + 64534 = 65536.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GID Mapping:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 0 100000 128&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 128 128 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 129 100129 873&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1002 1002 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1003 101003 64533&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify: 128 + 1 + 873 + 1 + 64533 = 65536.&lt;/p&gt;
&lt;p&gt;Notice the pattern: the gap between your two GID passthroughs (128 and 1002) is 1002 - 128 - 1 = 873. That&amp;rsquo;s the count for the middle range. The final range is always 65536 minus the sum of everything before it.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t forget: if you use different IDs, you also need to update &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; from Step 1 to match. Those files must list every host ID you&amp;rsquo;re passing through.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;Quick sanity check.&lt;/strong&gt; After writing your idmap lines, add up all the &lt;code&gt;count&lt;/code&gt; values for your &lt;code&gt;u&lt;/code&gt; lines and all the &lt;code&gt;count&lt;/code&gt; values for your &lt;code&gt;g&lt;/code&gt; lines. Both totals must equal exactly 65536. If either one doesn&amp;rsquo;t, your container will not start.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-5-clear-stale-mounts-before-restarting"&gt;Step 5: Clear Stale Mounts Before Restarting
&lt;/h2&gt;&lt;p&gt;Failed startups leave behind ghost mounts on the host. If you try to start the container again without cleaning these up, it can fail in new and exciting ways.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Only run this if your container previously failed to start. This forcibly unmounts staged mount points.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;On the host, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;umount -l /var/lib/lxc/.pve-staged-mounts/mp* 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;-l&lt;/code&gt; flag performs a lazy unmount, which detaches the filesystem immediately and cleans up references once they&amp;rsquo;re no longer busy. This is safe for cleaning up after failed mount attempts. One caveat: don&amp;rsquo;t use lazy unmount as a habit for mounts that keep failing. It detaches the mount without fixing whatever caused the failure, so if you find yourself running this repeatedly, something deeper is wrong. Go back and check your config.&lt;/p&gt;
&lt;p&gt;Now start your container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct start XXX
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-6-verify-everything-works"&gt;Step 6: Verify Everything Works
&lt;/h2&gt;&lt;p&gt;A clean start isn&amp;rsquo;t proof of success. Confirm permissions are correct end-to-end.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check that mounts exist and ownership is correct:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct enter XXX
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /media/Movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see your media files with the correct ownership. If the ownership looks wrong (everything owned by &lt;code&gt;nobody&lt;/code&gt; or &lt;code&gt;65534&lt;/code&gt;), your idmap entries are off. Go back to Step 4 and double-check your math.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test file operations (if using rw mounts):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Inside the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;touch /media/Downloads/testfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /media/Downloads/testfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm /media/Downloads/testfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The test file should be owned by your media user, not root or nobody.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confirm services start cleanly:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Inside the container, check your media service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status jellyfin &lt;span class="c1"&gt;# or plex, sonarr, radarr, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the service is running and can see your library, you&amp;rsquo;re done. If it starts but can&amp;rsquo;t find media files, the mount exists but the path inside the container doesn&amp;rsquo;t match what the service expects. Double-check the container path in your &lt;code&gt;lxc.mount.entry&lt;/code&gt; lines from Step 3.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check LXC logs for clean startup (from the host):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc-start -n XXX -F -l DEBUG -o /dev/stdout
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A clean startup will show your mount entries being applied without any &lt;code&gt;idmapped&lt;/code&gt; errors or permission denials. If you see warnings but the container still starts, investigate them. Warnings that you ignore today become outages after the next update.&lt;/p&gt;
&lt;h2 id="why-this-works"&gt;Why This Works
&lt;/h2&gt;&lt;p&gt;By switching from &lt;code&gt;mpX&lt;/code&gt; to &lt;code&gt;lxc.mount.entry&lt;/code&gt;, you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Skip Proxmox&amp;rsquo;s idmapped mount helpers entirely&lt;/li&gt;
&lt;li&gt;Bypass kernel-level idshift enforcement that FUSE filesystems can&amp;rsquo;t handle&lt;/li&gt;
&lt;li&gt;Let LXC translate IDs at runtime using your explicit mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how LXC handled unprivileged containers before Proxmox 9.1.5 added automatic idmapping. You&amp;rsquo;re not doing anything weird or unsupported. You&amp;rsquo;re being explicit about what Proxmox used to do implicitly.&lt;/p&gt;
&lt;p&gt;This configuration should survive future Proxmox updates because it relies on a stable LXC feature instead of a Proxmox-specific shortcut. That said, major version upgrades can always change defaults, so keep your config backups current and check release notes before upgrading.&lt;/p&gt;
&lt;h2 id="troubleshooting-status-30-errors"&gt;Troubleshooting Status 30 Errors
&lt;/h2&gt;&lt;h3 id="container-still-fails-with-status-30"&gt;Container Still Fails With Status 30
&lt;/h3&gt;&lt;p&gt;Work through this checklist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirm that no &lt;code&gt;mpX&lt;/code&gt; entries remain in &lt;code&gt;/etc/pve/lxc/XXX.conf&lt;/code&gt;. Grep for them: &lt;code&gt;grep '^mp' /etc/pve/lxc/XXX.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; include entries for every ID you&amp;rsquo;re passing through&lt;/li&gt;
&lt;li&gt;Check that your idmap ranges are contiguous and total 65536 for both UIDs and GIDs&lt;/li&gt;
&lt;li&gt;Confirm the host paths in your &lt;code&gt;lxc.mount.entry&lt;/code&gt; lines actually exist: &lt;code&gt;ls -la /media/storage/Movies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Clear stale mounts (Step 5) and try again&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="storage-appears-read-only-inside-the-container"&gt;Storage Appears Read-Only Inside the Container
&lt;/h3&gt;&lt;p&gt;Test write access directly on the host first:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;touch /mnt/media/testfile &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rm /mnt/media/testfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If this fails, the problem is at the storage level, not LXC. Check your MergerFS config, underlying disk health, and filesystem mount options. If the host can write but the container can&amp;rsquo;t, double-check that your &lt;code&gt;lxc.mount.entry&lt;/code&gt; line uses &lt;code&gt;rw&lt;/code&gt; and not &lt;code&gt;ro&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="nfs-mounts-fail"&gt;NFS Mounts Fail
&lt;/h3&gt;&lt;p&gt;NFS typically lacks idmapped mount support, so the same &lt;code&gt;lxc.mount.entry&lt;/code&gt; approach applies. The extra wrinkle: your NFS export must allow access from the UIDs and GIDs you&amp;rsquo;re mapping.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Check your &lt;code&gt;/etc/exports&lt;/code&gt; on the NFS server. Make sure &lt;code&gt;no_root_squash&lt;/code&gt; or appropriate user mapping is configured for the host IDs you&amp;rsquo;re passing through. This is easy to miss and will cause silent permission failures even if everything else is configured correctly.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="multiple-media-users-or-docker-inside-lxc"&gt;Multiple Media Users or Docker Inside LXC
&lt;/h3&gt;&lt;p&gt;If you have more than one media user that needs passthrough (for example, separate users for Sonarr and Jellyfin), you need to add a passthrough line for each UID and GID in Step 4 and a delegation entry for each in Step 1. The same splitting logic applies: break the range around each passthrough ID and make sure the counts still total 65536.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running Docker inside an unprivileged LXC container (first WHY?!), you&amp;rsquo;ll hit additional nesting issues. Docker needs its own ID namespace, which adds complexity on top of the LXC mapping. This is solvable but outside the scope of this guide. The Proxmox wiki has a section on nested containers that covers the basics.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What causes Read-only file system (os error 30) in Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Proxmox attempted a write through a mount that the kernel rejected. With MergerFS and unprivileged LXC containers, this almost always means the idmapped mount failed silently and the container got a read-only fallback. Switching from &lt;code&gt;mpX&lt;/code&gt; to &lt;code&gt;lxc.mount.entry&lt;/code&gt; fixes it.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is this specific to Proxmox 9.1.5?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Earlier Proxmox versions didn&amp;rsquo;t enforce idmapped mounts for unprivileged containers as aggressively. The kernel and LXC defaults changed in Proxmox 9 to make this behavior automatic.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I switch to privileged containers?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Privileged containers run as root on the host, which means a compromised container gives an attacker full access to your Proxmox node and every VM and container on it. Manual ID mapping gives you both compatibility and security. The extra configuration is worth it.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does this affect VMs too?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;VMs use a completely different storage model. They don&amp;rsquo;t use LXC bind mounts at all. However, MergerFS can still cause backup failures and disk I/O errors in VM environments for separate reasons.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will this survive the next Proxmox update?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The &lt;code&gt;lxc.mount.entry&lt;/code&gt; directive is a stable LXC feature and not a Proxmox hack. It should survive point releases without issue. Major version upgrades could change defaults, so always read release notes and keep config backups before upgrading.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Proxmox 9.1.5 tightened its security defaults, which is a good thing. The side effect is that FUSE-based filesystems like MergerFS and certain NFS configurations got caught in the crossfire.&lt;/p&gt;
&lt;p&gt;If you rely on MergerFS or NFS with unprivileged containers, the old &lt;code&gt;mpX&lt;/code&gt; approach is dead. Replacing it with &lt;code&gt;lxc.mount.entry&lt;/code&gt; and explicit ID mapping restores stability without giving up security.&lt;/p&gt;
&lt;p&gt;Once configured correctly, containers start cleanly and survive updates. Status 30 becomes a bad memory. It&amp;rsquo;s an hour of config work that saves you from this headache permanently.&lt;/p&gt;
&lt;h2 id="recommended-hardware"&gt;Recommended Hardware
&lt;/h2&gt;&lt;p&gt;The configuration above is what actually keeps MergerFS pools running under unprivileged LXCs. Here&amp;rsquo;s the hardware sitting behind it on my own host. Drives first, then the HBA that exposes them, then the cables that wire it all together.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;br&gt;
Solid high-capacity drive for MergerFS pools. I run several of these as the backing storage for the exact setup described in this guide. Not the cheapest per-TB option, but reliable and widely available.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0BWY1VH3V"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_a938d16ac34e6334.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_8994babafb301939.webp" width="600" height="431" alt="LSI 9211-8iB IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;LSI 9211-8iB IT MODE&lt;/strong&gt;&lt;br&gt;
An HBA flashed to IT mode passes drives directly to the OS without a hardware RAID layer. This is what you want for MergerFS or ZFS setups where the OS needs raw disk access. Cheap on eBay, rock-solid, and still the default recommendation in most homelab communities.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4iXST7J" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B088H2QLBQ"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/sff-8087_hu_51b302d89f9a5b28.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/sff-8087_hu_7efac13b7a7b21bc.webp" width="600" height="600" alt="SFF-8087 to 4x SATA" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;SFF-8087 to 4x SATA&lt;/strong&gt;&lt;br&gt;
You&amp;rsquo;ll need these breakout cables to connect SATA drives to the LSI HBA above. One cable handles four drives. Grab two if you&amp;rsquo;re filling a larger chassis.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/42fXWsQ" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>OPNsense vs pfSense for Homelabs (2026): Complete Comparison</title><link>https://diymediaserver.com/post/2026/opnsense-vs-pfsense-homelab-2026/</link><pubDate>Fri, 23 Jan 2026 08:22:37 -0700</pubDate><guid>https://diymediaserver.com/post/2026/opnsense-vs-pfsense-homelab-2026/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/opnsense-vs-pfsense-homelab-2026/featured_hu_464471b87935a4e.webp" alt="Featured image of post OPNsense vs pfSense for Homelabs (2026): Complete Comparison" /&gt;&lt;h2 id="what-are-opnsense-and-pfsense-and-why-it-matters-for-homelabs-in-2026"&gt;What are OPNsense and pfSense and Why It Matters for Homelabs in 2026
&lt;/h2&gt;&lt;p&gt;OPNsense versus pfSense is the most debated firewall choice in homelab communities. Both are FreeBSD-based firewalls that handle routing, VPNs, intrusion detection, and traffic management. They cover the same ground technically. The real differences come down to philosophy, user experience, and whether you trust the vendor not to change the deal later.&lt;/p&gt;
&lt;p&gt;I ran pfSense for years because it was &amp;ldquo;the standard.&amp;rdquo; Then Netgate started moving features to pfSense Plus. The line between &amp;ldquo;free&amp;rdquo; and &amp;ldquo;pay us&amp;rdquo; kept shifting. I woke up one morning and realized I was building critical infrastructure on a platform where the vendor could arbitrarily decide which features belonged to paying customers. I rebuilt on OPNsense that weekend.&lt;/p&gt;
&lt;p&gt;Neither firewall is objectively &amp;ldquo;better.&amp;rdquo; But one might fit your tolerance for corporate shenanigans a lot better.&lt;/p&gt;
&lt;p&gt;This guide is for homelabbers who want to pick one firewall, deploy it, and move on. You&amp;rsquo;ll know which one by the end.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Both OPNsense and pfSense are solid FreeBSD-based firewalls. If you want a modern UI, more built-in features, and development that won&amp;rsquo;t suddenly go closed-source, pick OPNsense. If you want conservative releases and a massive backlog of legacy documentation (and trust Netgate), pfSense works fine.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="quick-comparison-opnsense-vs-pfsense-at-a-glance"&gt;Quick Comparison: OPNsense vs pfSense at a Glance
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;OPNsense&lt;/th&gt;
&lt;th&gt;pfSense&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Fully Open Source&lt;/td&gt;
&lt;td&gt;CE: Open, Plus: Proprietary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI Style&lt;/td&gt;
&lt;td&gt;Modern sidebar navigation&lt;/td&gt;
&lt;td&gt;Traditional top menu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update Frequency&lt;/td&gt;
&lt;td&gt;Bi-yearly major releases&lt;/td&gt;
&lt;td&gt;Annual major releases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in IDS&lt;/td&gt;
&lt;td&gt;Yes (Suricata)&lt;/td&gt;
&lt;td&gt;Requires package install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard&lt;/td&gt;
&lt;td&gt;Built-in by default&lt;/td&gt;
&lt;td&gt;Requires plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Patches&lt;/td&gt;
&lt;td&gt;Days after FreeBSD&lt;/td&gt;
&lt;td&gt;CE waits for Plus first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;Modern workflows, open-source advocates&lt;/td&gt;
&lt;td&gt;Conservative updates, legacy setups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="core-similarities-why-this-choice-is-hard"&gt;Core Similarities: Why This Choice Is Hard
&lt;/h2&gt;&lt;p&gt;Both platforms do the same basic job:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD-based firewall and router&lt;/li&gt;
&lt;li&gt;Stateful packet inspection and NAT&lt;/li&gt;
&lt;li&gt;VLANs, LAGG (bond NICs together), multi-WAN failover&lt;/li&gt;
&lt;li&gt;VPN: IPsec, OpenVPN, WireGuard&lt;/li&gt;
&lt;li&gt;Runs on bare metal or VMs&lt;/li&gt;
&lt;li&gt;Works great on Proxmox&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For basic WAN-to-LAN routing and site-to-site VPN, either one will work. You could pick based on a coin flip and be fine.&lt;/p&gt;
&lt;p&gt;The differences matter when you live with the system for months or years.&lt;/p&gt;
&lt;h2 id="ui-and-usability-opnsense-vs-pfsense-interface-comparison"&gt;UI and Usability: OPNsense vs pfSense Interface Comparison
&lt;/h2&gt;&lt;h3 id="opnsense-ui-philosophy"&gt;OPNsense UI Philosophy
&lt;/h3&gt;&lt;p&gt;OPNsense broke from pfSense&amp;rsquo;s interface years ago. That decision has paid off.&lt;/p&gt;
&lt;p&gt;The UI uses a left-side collapsible menu. Interfaces are under Interfaces. Firewall rules are under Firewall. Services are under Services. Interface descriptions are editable during assignment instead of buried three clicks deep where you&amp;rsquo;ll never find them again.&lt;/p&gt;
&lt;p&gt;Virtual NICs live under Interfaces; pfSense buries them under Firewall. Intrusion Detection lives under Services rather than hidden in a package submenu. When you&amp;rsquo;re trying to fix something late at night, you won&amp;rsquo;t spend five minutes hunting for the setting you need.&lt;/p&gt;
&lt;p&gt;If you experiment, break things, and rebuild regularly, this consistency may keep you sane.&lt;/p&gt;
&lt;h3 id="pfsense-ui-philosophy"&gt;pfSense UI Philosophy
&lt;/h3&gt;&lt;p&gt;pfSense uses a top navigation bar with nested dropdowns. It works. It also shows its age.&lt;/p&gt;
&lt;p&gt;Strengths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More default dashboard widgets (about 22 vs OPNsense&amp;rsquo;s 17)&lt;/li&gt;
&lt;li&gt;Consistent with documentation from 2015&lt;/li&gt;
&lt;li&gt;Familiar if you&amp;rsquo;ve used pfSense for years&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Weaknesses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Settings scattered across Firewall, System, and Services (good luck)&lt;/li&gt;
&lt;li&gt;Interface descriptions require extra clicks after assignment&lt;/li&gt;
&lt;li&gt;Package settings bolted onto the side like afterthoughts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you learned pfSense first, you know where everything is. If you&amp;rsquo;re new, you&amp;rsquo;ll spend time hunting. I&amp;rsquo;ve watched people stare at the top menu for 30 seconds trying to remember where DHCP server settings live. (Services, if you&amp;rsquo;re wondering.)&lt;/p&gt;
&lt;h3 id="real-world-ui-example-setting-up-vlans"&gt;Real-World UI Example: Setting Up VLANs
&lt;/h3&gt;&lt;p&gt;To create a guest VLAN with internet access but blocked LAN access:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Interfaces &amp;gt; Other Types &amp;gt; VLAN (create VLAN 10)&lt;/li&gt;
&lt;li&gt;Interfaces &amp;gt; Assignments (assign to OPT1)&lt;/li&gt;
&lt;li&gt;Firewall &amp;gt; Rules &amp;gt; OPT1 (add allow-internet rule)&lt;/li&gt;
&lt;li&gt;Firewall &amp;gt; Rules &amp;gt; OPT1 (add block-LAN rule above it)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;pfSense:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Interfaces &amp;gt; Assignments &amp;gt; VLANs (create VLAN 10)&lt;/li&gt;
&lt;li&gt;Interfaces &amp;gt; Assignments (assign to OPT1)&lt;/li&gt;
&lt;li&gt;Interfaces &amp;gt; OPT1 (enable and configure)&lt;/li&gt;
&lt;li&gt;Firewall &amp;gt; Rules &amp;gt; OPT1 (add rules)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both work. OPNsense puts VLANs under Interfaces where you&amp;rsquo;d look for them. pfSense buries VLAN creation under Assignments. One extra click that always feels wrong.&lt;/p&gt;
&lt;p&gt;UI clarity matters? Pick OPNsense. Years of muscle memory? Stick with pfSense.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;
Fanless, silent, and shipped with six Intel i226-V 2.5GbE ports out of the box. If you want to compare both platforms fairly, this box has two M.2 slots. Keep an OPNsense install on one NVMe drive and pfSense on the other, then swap between them. The tradeoff is cost. You&amp;rsquo;ll pay more than repurposing an old desktop.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="plugin-ecosystems-and-built-in-features"&gt;Plugin Ecosystems and Built-In Features
&lt;/h2&gt;&lt;h3 id="opnsense-more-included-by-default"&gt;OPNsense: More Included by Default
&lt;/h3&gt;&lt;p&gt;OPNsense ships with these features enabled:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intrusion Detection (watches your network traffic, alerts on sketchy behavior)&lt;/li&gt;
&lt;li&gt;Traffic reporting dashboards&lt;/li&gt;
&lt;li&gt;Monit service monitoring (restarts dead services automatically)&lt;/li&gt;
&lt;li&gt;CPU, memory, disk widgets&lt;/li&gt;
&lt;li&gt;Built-in WireGuard VPN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can enable most of these without installing a plugin. Fewer plugins mean fewer things that can break during upgrades. (I learned this the hard way with pfBlockerNG, which ate an entire Saturday once.)&lt;/p&gt;
&lt;p&gt;Optional plugins exist for SMART monitoring, Wake-on-LAN, and Zenarmor traffic inspection. But the core firewall works fine without them.&lt;/p&gt;
&lt;h3 id="pfsense-package-driven-power"&gt;pfSense: Package-Driven Power
&lt;/h3&gt;&lt;p&gt;pfSense CE relies more on packages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Suricata or Snort for intrusion detection&lt;/li&gt;
&lt;li&gt;ntopng for traffic analysis (shows you what&amp;rsquo;s eating bandwidth)&lt;/li&gt;
&lt;li&gt;SMART monitoring&lt;/li&gt;
&lt;li&gt;Additional dashboards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The package manager is powerful. It&amp;rsquo;s also a maintenance risk. When pfSense updates the core system, packages sometimes lag. Sometimes they break. Sometimes they stop working entirely until the maintainer catches up.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the CE/Plus split:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pfSense CE: free, open-source (for now)&lt;/li&gt;
&lt;li&gt;pfSense Plus: closed-source features, faster updates, tied to Netgate hardware or subscriptions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters if you care whether your firewall config depends on features that could disappear behind a paywall. I don&amp;rsquo;t want to wake up one day and find out the feature I rely on is Plus-only now.&lt;/p&gt;
&lt;p&gt;Fewer plugins and more built-in functionality? OPNsense. Specific pfSense packages you can&amp;rsquo;t live without? pfSense.&lt;/p&gt;
&lt;h3 id="the-pfblockerng-lesson"&gt;The pfBlockerNG Lesson
&lt;/h3&gt;&lt;p&gt;I ran pfBlockerNG for ad blocking and threat feeds. Worked great for eight months. Then a pfSense core update hit. pfBlockerNG didn&amp;rsquo;t update in time. The firewall booted fine. DNS stopped working completely.&lt;/p&gt;
&lt;p&gt;Spent three hours troubleshooting. Checked DNS forwarder settings. Verified upstream resolvers. Restarted services. Nothing. The logs showed DNS queries arriving but pfBlockerNG was silently dropping everything because its threat feed database was incompatible with the new pfSense version.&lt;/p&gt;
&lt;p&gt;Removed pfBlockerNG. DNS came back instantly.&lt;/p&gt;
&lt;p&gt;This is the package problem in miniature. When your ad blocker can take down your entire network and the logs don&amp;rsquo;t quite tell you why, you start questioning your architecture. That Saturday convinced me to rebuild on a platform with fewer external dependencies.&lt;/p&gt;
&lt;h2 id="opnsense-vs-pfsense-performance-and-hardware-requirements"&gt;OPNsense vs pfSense Performance and Hardware Requirements
&lt;/h2&gt;&lt;h3 id="hardware-requirements-and-virtualization"&gt;Hardware Requirements and Virtualization
&lt;/h3&gt;&lt;p&gt;For self-hosting, OPNsense hardware requirements match pfSense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;x86-64 CPU&lt;/li&gt;
&lt;li&gt;4 GB RAM minimum, 8 GB recommended&lt;/li&gt;
&lt;li&gt;2 NICs minimum (Intel NICs strongly recommended, Realtek will make you question your sanity)&lt;/li&gt;
&lt;li&gt;SSD storage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Proxmox Deployment:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Both run well as VMs. Pass through physical NICs or use virtio adapters. Give it 2 vCPUs minimum, 4+ if you&amp;rsquo;re running IDS. Hardware offloading can cause weird issues with some hypervisors, test it. Back up your configs before hypervisor updates. (You do this already, right?)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve run both on Proxmox with 4 vCPUs and 8GB RAM handling 500Mbps WAN without issues.&lt;/p&gt;
&lt;h3 id="real-world-performance"&gt;Real-World Performance
&lt;/h3&gt;&lt;p&gt;Both deliver similar throughput on identical hardware. The bottleneck is your NIC or CPU, not the firewall software.&lt;/p&gt;
&lt;p&gt;On a quad-core i5 with Intel NICs, expect near line-rate for basic routing (900+ Mbps on gigabit). WireGuard pushes 600-800 Mbps. OpenVPN is CPU-bound, usually 200-400 Mbps. Turn on IDS and lose 10-30% depending on rulesets.&lt;/p&gt;
&lt;p&gt;If your numbers are terrible, it&amp;rsquo;s probably hardware offloading or bad NIC drivers. (Realtek, I&amp;rsquo;m looking at you.)&lt;/p&gt;
&lt;p&gt;Performance is a tie. Choose based on other factors.&lt;/p&gt;
&lt;h3 id="performance-benchmarks-real-numbers"&gt;Performance Benchmarks: Real Numbers
&lt;/h3&gt;&lt;p&gt;Tested on identical hardware (i5-8500, 16GB RAM, Intel i350-T4 NICs):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Routing Performance:&lt;/strong&gt;
OPNsense: 940 Mbps WAN-to-LAN (line rate)
pfSense: 938 Mbps WAN-to-LAN (line rate)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WireGuard VPN:&lt;/strong&gt;
OPNsense: 720 Mbps
pfSense: 710 Mbps&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenVPN:&lt;/strong&gt;
OPNsense: 380 Mbps
pfSense: 375 Mbps&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With IDS Enabled (Suricata, 3 rulesets):&lt;/strong&gt;
OPNsense: 680 Mbps (-27%)
pfSense: 670 Mbps (-28%)&lt;/p&gt;
&lt;p&gt;Performance difference is negligible. Your hardware matters more than your platform.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
These aren&amp;rsquo;t my numbers. A friend with a 1 Gbps line ran the tests. Made for easier math.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="update-philosophy-opnsense-vs-pfsense-release-cycle"&gt;Update Philosophy: OPNsense vs pfSense Release Cycle
&lt;/h2&gt;&lt;h3 id="opnsense-updates"&gt;OPNsense Updates
&lt;/h3&gt;&lt;p&gt;OPNsense follows a predictable schedule:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Major releases twice yearly (January and July)&lt;/li&gt;
&lt;li&gt;Security updates and patches as needed&lt;/li&gt;
&lt;li&gt;Plugin updates independent of core system&lt;/li&gt;
&lt;li&gt;Clear change logs and migration guides&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The web UI shows pending updates with one-click installation. Rollback options exist if something breaks. Development is transparent. Community input matters.&lt;/p&gt;
&lt;p&gt;Updates feel modern and reliable.&lt;/p&gt;
&lt;h3 id="pfsense-updates"&gt;pfSense Updates
&lt;/h3&gt;&lt;p&gt;pfSense CE takes a more conservative approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Major releases roughly annually&lt;/li&gt;
&lt;li&gt;Minor updates and security patches as needed&lt;/li&gt;
&lt;li&gt;pfSense Plus gets updates first&lt;/li&gt;
&lt;li&gt;CE trails behind (sometimes weeks)&lt;/li&gt;
&lt;li&gt;Some features migrate to Plus-only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This frustrates people. CE users wait for security patches that Plus users already have. You&amp;rsquo;re constantly aware of what you&amp;rsquo;re missing. The free tier feels like a free tier.&lt;/p&gt;
&lt;p&gt;For &amp;ldquo;set it and forget it&amp;rdquo; homelabs, pfSense&amp;rsquo;s slower pace could be a feature or a frustration. Depends on your perspective.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mid-2026 check-in.&lt;/strong&gt; The split isn&amp;rsquo;t one-directional. pfSense CE 2.8 (2025) actually pulled several previously Plus-only features back into the free edition, and as of June 2026 CE 2.8.1 is current while pfSense Plus hasn&amp;rsquo;t shipped a release since 24.11 in November 2024. The CE/Plus gap widens and narrows with Netgate&amp;rsquo;s priorities. This unpredictability is the real issue. OPNsense, by contrast, kept its clockwork pace: the 26.1 series landed in January 2026, with 26.7 due in July.&lt;/p&gt;
&lt;p&gt;Regular updates and transparency? OPNsense. Conservative updates and slower pace? pfSense.&lt;/p&gt;
&lt;p&gt;Whichever cadence you pick, you still need something to run it on. For a first homelab firewall on a budget, a used micro PC is hard to beat. Add a NIC and you&amp;rsquo;re done.&lt;/p&gt;
&lt;div class="product-box" data-asin="B08C4QM3DF"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/dell7070_hu_576d9bc7347def2.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/dell7070_hu_6536d4eb8e384c52.webp" width="600" height="1172" alt="Dell OptiPlex 7070 Micro" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Dell OptiPlex 7070 Micro&lt;/strong&gt;
Cheap, quiet, and easy to repurpose as a pfSense or OPNsense box. Perfect first firewall hardware. The catch: it ships with a single NIC, so you&amp;rsquo;ll need to add a USB 2.5GbE adapter or a PCIe NIC (on the bigger SFF chassis) to handle WAN and LAN properly.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JkmsDm" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="trust-and-long-term-viability"&gt;Trust and Long-Term Viability
&lt;/h2&gt;&lt;p&gt;This is where it gets personal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fully open-source, no proprietary split&lt;/li&gt;
&lt;li&gt;Community-driven development&lt;/li&gt;
&lt;li&gt;No vendor lock-in&lt;/li&gt;
&lt;li&gt;Deciso B.V. sponsors but doesn&amp;rsquo;t control features&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;pfSense:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Netgate controls everything&lt;/li&gt;
&lt;li&gt;CE is open, Plus is closed&lt;/li&gt;
&lt;li&gt;CE feels like the free tier of a paid product&lt;/li&gt;
&lt;li&gt;Netgate&amp;rsquo;s business goals don&amp;rsquo;t align with homelab users&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I switched to OPNsense because I don&amp;rsquo;t want my firewall&amp;rsquo;s future tied to quarterly earnings calls. pfSense CE isn&amp;rsquo;t dying tomorrow. But I don&amp;rsquo;t like the trajectory.&lt;/p&gt;
&lt;p&gt;When a company starts moving features behind paywalls, it doesn&amp;rsquo;t stop. It accelerates. I&amp;rsquo;ve seen this before.&lt;/p&gt;
&lt;p&gt;Open-source purity matters? OPNsense. Netgate ecosystem matters more? pfSense.&lt;/p&gt;
&lt;h2 id="my-migration-weekend-what-happened"&gt;My Migration Weekend: What Happened
&lt;/h2&gt;&lt;p&gt;I ran pfSense for four years before switching. The migration took six hours over one weekend.&lt;/p&gt;
&lt;h3 id="saturday---setup-and-config-migration"&gt;Saturday - Setup and Config Migration
&lt;/h3&gt;&lt;p&gt;Spun up OPNsense VM on Proxmox. Exported pfSense config to XML, tried importing. It accepted the file but only 60% transferred cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt; Interface assignments, VLANs, basic firewall rules, DHCP scopes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; NAT rules needed manual recreation. VPN configs had to be rebuilt from scratch. DNS forwarder settings didn&amp;rsquo;t transfer.&lt;/p&gt;
&lt;p&gt;WireGuard rebuild: 20 minutes.&lt;/p&gt;
&lt;h3 id="sunday---testing-and-cutover"&gt;Sunday - Testing and Cutover
&lt;/h3&gt;&lt;p&gt;Ran both firewalls in parallel for testing. Shut down pfSense, changed VLAN assignments on switch, updated DHCP gateway IPs.&lt;/p&gt;
&lt;p&gt;Total downtime: about 20 minutes.&lt;/p&gt;
&lt;p&gt;Kept pfSense VM around for two weeks as backup. Never needed it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I&amp;rsquo;d Do Differently&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Export VPN client configs before starting. I had to message six people with new configs during the cutover.&lt;/p&gt;
&lt;p&gt;Test VLAN isolation more thoroughly. Found a misconfigured rule Monday morning that let guest traffic reach management VLAN. Fixed in five minutes but should&amp;rsquo;ve caught it Sunday.&lt;/p&gt;
&lt;p&gt;Been running OPNsense for 18 months now. No regrets.&lt;/p&gt;
&lt;h2 id="when-you-should-stay-on-pfsense"&gt;When You Should Stay on pfSense
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t switch if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You have complex pfBlockerNG configs you can&amp;rsquo;t easily recreate.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zenarmor exists on OPNsense but it&amp;rsquo;s not identical. If you&amp;rsquo;ve got custom threat feeds and DNSBL configs that took months to tune, migration pain might not be worth it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Your network depends on pfSense-specific packages.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some packages don&amp;rsquo;t have OPNsense equivalents. Check before committing to migration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You have working configs and no pain points.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Migration has costs. If pfSense works, and you&amp;rsquo;re not frustrated, stay put. I switched because the CE/Plus split bothered me and I had a package break. If you don&amp;rsquo;t have these problems, you don&amp;rsquo;t need to solve them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="practical-decision-guide-for-homelab-firewalls"&gt;Practical Decision Guide for Homelab Firewalls
&lt;/h2&gt;&lt;p&gt;Ask yourself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modern UI that reduces mistakes? → &lt;strong&gt;OPNsense&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Rely on specific pfSense packages? → &lt;strong&gt;pfSense&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Care about open-source purity? → &lt;strong&gt;OPNsense&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Want ultra-conservative updates? → &lt;strong&gt;pfSense&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Run everything in Proxmox VMs? → Either works, &lt;strong&gt;OPNsense&lt;/strong&gt; slightly easier&lt;/li&gt;
&lt;li&gt;Need maximum plugin flexibility? → &lt;strong&gt;pfSense&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re undecided, install both in VMs. Spend an afternoon configuring VLANs and WireGuard. Your preference will become obvious. Don&amp;rsquo;t agonize over this for weeks. Spin them up, click around, pick one.&lt;/p&gt;
&lt;h2 id="decision-tree-which-firewall-should-you-pick"&gt;Decision Tree: Which Firewall Should You Pick?
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Do you already run pfSense with no issues?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YES → Stay on pfSense (don&amp;rsquo;t fix what isn&amp;rsquo;t broken).&lt;/li&gt;
&lt;li&gt;NO → Continue&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do you rely on pfSense-specific packages?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YES → Stay on pfSense (migration pain isn&amp;rsquo;t worth it).&lt;/li&gt;
&lt;li&gt;NO → Continue&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Does the CE/Plus split bother you?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YES → Switch to OPNsense.&lt;/li&gt;
&lt;li&gt;NO → Continue&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do you want a modern UI with better organization?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YES → Switch to OPNsense.&lt;/li&gt;
&lt;li&gt;NO → Continue&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Do you want faster security updates?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YES → Switch to OPNsense.&lt;/li&gt;
&lt;li&gt;NO → Stay on pfSense (conservative updates suit you).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you end up at &amp;ldquo;Stay on pfSense&amp;rdquo; but still feel uncertain, that uncertainty is telling you something. Listen to it.&lt;/p&gt;
&lt;h2 id="common-mistakes-that-will-bite-you"&gt;Common Mistakes That Will Bite You
&lt;/h2&gt;&lt;h3 id="dont-virtualize-on-hardware-youre-using-for-other-things"&gt;Don&amp;rsquo;t Virtualize on Hardware You&amp;rsquo;re Using for Other Things
&lt;/h3&gt;&lt;p&gt;Your firewall VM needs dedicated hardware or a hypervisor that&amp;rsquo;s always on. I&amp;rsquo;ve watched people wonder why their network dies when they reboot their workstation to install updates. Because your firewall is on it. Obviously.&lt;/p&gt;
&lt;p&gt;Run your firewall on a dedicated box, a separate hypervisor, or accept that rebooting your daily driver takes down your entire network. There&amp;rsquo;s no middle ground here.&lt;/p&gt;
&lt;h3 id="dont-skip-backups-before-updates"&gt;Don&amp;rsquo;t Skip Backups Before Updates
&lt;/h3&gt;&lt;p&gt;Both platforms make this easy:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense:&lt;/strong&gt; System &amp;gt; Configuration &amp;gt; Backups &amp;gt; Download configuration&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pfSense:&lt;/strong&gt; Diagnostics &amp;gt; Backup &amp;amp; Restore &amp;gt; Download configuration as XML&lt;/p&gt;
&lt;p&gt;Save it locally with the date in the filename: &lt;code&gt;firewall-backup-2026-01-31.xml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Do this before updates. Do this before changing anything important. Do this monthly even if you&amp;rsquo;re not changing anything. Store it somewhere that&amp;rsquo;s not the firewall. Your NAS, your workstation, cloud storage, doesn&amp;rsquo;t matter. Anywhere but the firewall itself.&lt;/p&gt;
&lt;p&gt;When (not if) you need to restore, you&amp;rsquo;ll thank past-you for being paranoid.&lt;/p&gt;
&lt;h3 id="dont-enable-every-ids-rule"&gt;Don&amp;rsquo;t Enable Every IDS Rule
&lt;/h3&gt;&lt;p&gt;More rules ≠ more security. You&amp;rsquo;ll kill performance and get flooded with false positives you&amp;rsquo;ll ignore.&lt;/p&gt;
&lt;p&gt;Start with recommended rulesets. Monitor for a week. Add more only if you need them. I ran with three rulesets for 18 months before adding a fourth. You don&amp;rsquo;t need 47 different threat feeds.&lt;/p&gt;
&lt;h3 id="dont-use-realtek-nics-if-you-can-avoid-it"&gt;Don&amp;rsquo;t Use Realtek NICs If You Can Avoid It
&lt;/h3&gt;&lt;p&gt;They work. They also cause weird throughput issues, driver headaches, and inexplicable packet loss under load.&lt;/p&gt;
&lt;p&gt;Intel NICs cost $20 more used on eBay. Buy Intel. The i350-T2 and i350-T4 are solid choices. Your future self will appreciate it when you&amp;rsquo;re not troubleshooting phantom network issues on a deadline.&lt;/p&gt;
&lt;h3 id="dont-trust-default-settings-for-production"&gt;Don&amp;rsquo;t Trust Default Settings for Production
&lt;/h3&gt;&lt;p&gt;Both platforms ship with sensible defaults for home use. But &amp;ldquo;sensible defaults&amp;rdquo; means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No firewall rules blocking RFC1918 (private IP addresses) traffic on WAN (fine for home, terrible for dual-WAN or VPS)&lt;/li&gt;
&lt;li&gt;DNS resolver allowing queries from all interfaces (convenient but insecure)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Review the defaults. Adjust for your environment. Lock down access to the web UI. Enable stricter firewall rules. Don&amp;rsquo;t assume &amp;ldquo;default&amp;rdquo; means &amp;ldquo;secure.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="minimum-viable-firewall-setup"&gt;Minimum Viable Firewall Setup
&lt;/h2&gt;&lt;p&gt;Stop overthinking the initial config. Day one, you need:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WAN interface configured&lt;/strong&gt; - DHCP from ISP or static IP, whichever your ISP provides&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LAN interface with DHCP enabled&lt;/strong&gt; - Both do this automatically during install&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default allow rule on LAN&lt;/strong&gt; - Both create this automatically&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS set to upstream resolvers&lt;/strong&gt; - 1.1.1.1 and 8.8.8.8, or your preference&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;rsquo;s it. Everything else is optional.&lt;/p&gt;
&lt;p&gt;VLANs? Add them when you need device isolation.
VPNs? Add them when you need remote access.
IDS? Add it when you want visibility into traffic.
Custom dashboards? Add them when the defaults feel limiting.&lt;/p&gt;
&lt;p&gt;Start simple. Add complexity only when you have a specific need. Your firewall&amp;rsquo;s job is routing packets and blocking threats. It does not need to look impressive in screenshots.&lt;/p&gt;
&lt;h2 id="troubleshooting-common-opnsense-and-pfsense-issues"&gt;Troubleshooting Common OPNsense and pfSense Issues
&lt;/h2&gt;&lt;h3 id="internet-works-but-throughput-is-terrible"&gt;Internet Works, but Throughput Is Terrible
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;What you see:&lt;/strong&gt; Slow speeds despite fast connection&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;
Disable hardware offloading first. This is the most common culprit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense:&lt;/strong&gt; Interfaces &amp;gt; Settings &amp;gt; Disable &amp;ldquo;Hardware CRC&amp;rdquo;, &amp;ldquo;Hardware TSO&amp;rdquo;, &amp;ldquo;Hardware LRO&amp;rdquo;
&lt;strong&gt;pfSense:&lt;/strong&gt; System &amp;gt; Advanced &amp;gt; Networking &amp;gt; Disable all hardware checksum offloading&lt;/p&gt;
&lt;p&gt;Reboot. Test again.&lt;/p&gt;
&lt;p&gt;If that doesn&amp;rsquo;t fix it, check IDS rulesets. Too many active rules kills performance. System &amp;gt; Intrusion Detection &amp;gt; Download &amp;gt; verify only 2-3 rulesets are enabled.&lt;/p&gt;
&lt;p&gt;Verify your NIC drivers are loaded correctly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# SSH into firewall, check what driver your NIC is using&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ifconfig -a &lt;span class="p"&gt;|&lt;/span&gt; grep -A &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;em0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see &amp;ldquo;re0&amp;rdquo; (Realtek), you found your problem. Intel NICs use &amp;ldquo;em&amp;rdquo;, &amp;ldquo;igb&amp;rdquo;, or &amp;ldquo;ix&amp;rdquo; drivers. Realtek uses &amp;ldquo;re&amp;rdquo;. Replace the NIC.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re seeing 100 Mbps on gigabit, the NIC negotiated wrong. Check System &amp;gt; Interfaces &amp;gt; [Interface] and verify it&amp;rsquo;s set to auto-negotiate or manually force 1000baseT full-duplex.&lt;/p&gt;
&lt;h3 id="vpn-is-slow"&gt;VPN Is Slow
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;What you see:&lt;/strong&gt; WireGuard or OpenVPN performing poorly&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;
Verify WireGuard is using kernel implementation (not userspace).
Check VPN &amp;gt; WireGuard &amp;gt; Instances &amp;gt; verify &amp;ldquo;Type&amp;rdquo; shows kernel implementation.&lt;/p&gt;
&lt;p&gt;Disable unnecessary logging (verbose logging kills performance).
VPN &amp;gt; WireGuard &amp;gt; Advanced &amp;gt; set log level to &amp;ldquo;error&amp;rdquo; only.&lt;/p&gt;
&lt;p&gt;Test without IDS. Suricata inspecting VPN traffic = very slow.
Services &amp;gt; Intrusion Detection &amp;gt; disable temporarily, test VPN speed.&lt;/p&gt;
&lt;p&gt;WireGuard should be fast. If it&amp;rsquo;s not, your config or hardware is wrong.&lt;/p&gt;
&lt;h3 id="upgrade-broke-something"&gt;Upgrade Broke Something
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;What you see:&lt;/strong&gt; Features missing or broken after update&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;
Restore config backup (you made one, right?).
System &amp;gt; Configuration &amp;gt; Backups &amp;gt; restore previous config.&lt;/p&gt;
&lt;p&gt;Check package compatibility in changelogs.
Before updating, read the release notes. They list package compatibility issues.
Review deprecated features list. Sometimes features get removed. Check migration guides.&lt;/p&gt;
&lt;p&gt;Test packages individually after core upgrade.
Update core first, reboot, then update packages one at a time.&lt;/p&gt;
&lt;p&gt;Restore your backup. Try again. Five minutes reading release notes can save an hour troubleshooting. (Yes, I have learned this the hard way.)&lt;/p&gt;
&lt;h2 id="security-differences-that-matter"&gt;Security Differences That Matter
&lt;/h2&gt;&lt;p&gt;Both platforms are secure by default. The differences are operational, not architectural.&lt;/p&gt;
&lt;h3 id="opnsense-security-advantages"&gt;OPNsense Security Advantages
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Two-factor authentication built-in.&lt;/strong&gt; No plugin needed. System &amp;gt; Access &amp;gt; Users &amp;gt; [Select User] &amp;gt; Generate new secret. Works with Google Authenticator, Authy, or any TOTP app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IDS integrated and easier to configure.&lt;/strong&gt; Services &amp;gt; Intrusion Detection &amp;gt; Download tab. Select rulesets, enable IDS, done. No separate package installation or config files to manage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Faster security patches.&lt;/strong&gt; No CE/Plus delay. When a FreeBSD security advisory drops, OPNsense patches hit within days. pfSense CE users wait for Plus to get patched first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More frequent security updates.&lt;/strong&gt; Bi-yearly major releases plus security patches as needed. pfSense CE releases are annual with longer gaps between security updates.&lt;/p&gt;
&lt;h3 id="pfsense-security-advantages"&gt;pfSense Security Advantages
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;More mature IDS rulesets.&lt;/strong&gt; Snort has been around longer than Suricata. More documentation, more tuned rules for specific scenarios. If you need very specific detection rules for niche attacks, pfSense&amp;rsquo;s Snort documentation is a deep rabbit hole.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More third-party security plugins.&lt;/strong&gt; ntopng integration is tighter. More options for anomaly detection and traffic analysis. If you want to build a full security monitoring stack, pfSense has more plugin options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More documented attack mitigation examples.&lt;/strong&gt; Decade of forum posts, blog tutorials, and Stack Overflow answers. If you&amp;rsquo;re mitigating a specific attack, someone&amp;rsquo;s documented how to do it on pfSense.&lt;/p&gt;
&lt;h3 id="what-matters"&gt;What Matters
&lt;/h3&gt;&lt;p&gt;Neither platform has had a major security incident in recent years. Your security posture depends more on configuration than platform choice.&lt;/p&gt;
&lt;p&gt;Common security mistakes I see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web UI exposed to WAN (don&amp;rsquo;t do this)&lt;/li&gt;
&lt;li&gt;Default admin passwords (change them immediately)&lt;/li&gt;
&lt;li&gt;No firewall rules blocking RFC1918 on WAN (matters for dual-WAN setups)&lt;/li&gt;
&lt;li&gt;Permissive outbound rules on LAN (most people allow all, should be more restrictive)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fix these regardless of platform. A misconfigured OPNsense box is less secure than a properly configured pfSense box, and vice versa.&lt;/p&gt;
&lt;h2 id="what-surprised-me-after-switching"&gt;What Surprised Me After Switching
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;OPNsense updates are faster.&lt;/strong&gt; pfSense updates took 10-15 minutes and always required a reboot. OPNsense updates finish in 2-3 minutes. Most don&amp;rsquo;t need a reboot. The few that do reboot in under 60 seconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The documentation is different.&lt;/strong&gt; pfSense has more forum posts and third-party tutorials dating back to 2008. Google any pfSense problem and you&amp;rsquo;ll find 47 blog posts about it. OPNsense has cleaner official docs but fewer community tutorials. Took me a few weeks to adjust to reading official docs instead of blog posts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Built-in features I didn&amp;rsquo;t know I wanted.&lt;/strong&gt; Monit caught a failing DNS resolver once and restarted it before I noticed. I woke up, checked logs, saw &amp;ldquo;unbound died, Monit restarted it 3 hours ago.&amp;rdquo; That alone justified the switch. On pfSense I would&amp;rsquo;ve woken up to &amp;ldquo;DNS is broken&amp;rdquo; messages from family.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The community is smaller but more active.&lt;/strong&gt; pfSense has more users. OPNsense has more engaged users. Forum questions get answered faster on OPNsense because there are fewer &amp;ldquo;have you tried turning it off and on again&amp;rdquo; responses. People assume competence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plugin updates don&amp;rsquo;t break things.&lt;/strong&gt; Because most features are built-in, there are fewer plugins to break during core updates. I haven&amp;rsquo;t had a plugin break in 18 months on OPNsense. On pfSense, I had plugin breakage every 3-4 months.&lt;/p&gt;
&lt;h2 id="quick-wins-after-installation"&gt;Quick Wins After Installation
&lt;/h2&gt;&lt;p&gt;First 30 minutes with either platform:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change default admin password&lt;/li&gt;
&lt;li&gt;Enable automatic config backups&lt;/li&gt;
&lt;li&gt;Set up 2FA (OPNsense: built-in, pfSense: use package)&lt;/li&gt;
&lt;li&gt;Configure DNS over TLS (prevents ISP snooping)&lt;/li&gt;
&lt;li&gt;Enable basic IDS with recommended rulesets&lt;/li&gt;
&lt;li&gt;Test failover to backup DNS resolver&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do these immediately. Everything else can wait.&lt;/p&gt;
&lt;h2 id="what-i-was-wrong-about"&gt;What I Was Wrong About
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;I thought migration would take all weekend.&lt;/strong&gt; Took six hours of actual work. Most of that was rebuilding OpenVPN configs because I didn&amp;rsquo;t export them properly first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I thought I&amp;rsquo;d miss pfSense packages.&lt;/strong&gt; Haven&amp;rsquo;t needed a single pfSense-specific package in 18 months. Everything I relied on either exists natively in OPNsense or has an equivalent plugin.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I thought OPNsense would be less stable.&lt;/strong&gt; It&amp;rsquo;s been rock-solid. Only reboots are for updates. Uptime between reboots averages 6-8 weeks. On pfSense I was rebooting every 3-4 weeks when packages broke.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I thought the smaller community would be a problem.&lt;/strong&gt; Smaller community means better signal-to-noise ratio. Questions get answered by people who actually know the codebase, not people guessing based on pfSense experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I thought performance would be identical.&lt;/strong&gt; It is, mostly. But OPNsense&amp;rsquo;s update speed and reboot time makes maintenance faster. Shaving 10 minutes off update time doesn&amp;rsquo;t sound like much until you&amp;rsquo;re doing it monthly.&lt;/p&gt;
&lt;h2 id="when-your-firewall-is-good-enough"&gt;When Your Firewall Is Good Enough
&lt;/h2&gt;&lt;p&gt;Stop tweaking when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WAN to LAN routing works at line rate&lt;/li&gt;
&lt;li&gt;VPNs connect reliably and stay connected&lt;/li&gt;
&lt;li&gt;You haven&amp;rsquo;t touched the config in a month&lt;/li&gt;
&lt;li&gt;Uptime is measured in weeks, not hours&lt;/li&gt;
&lt;li&gt;Family/roommates don&amp;rsquo;t complain about the network&lt;/li&gt;
&lt;li&gt;You stop checking the dashboard daily&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your firewall&amp;rsquo;s job is to be invisible. Once it disappears into the background, you&amp;rsquo;ve won. Move on to other projects.&lt;/p&gt;
&lt;p&gt;The best firewall is the one you forget about. If you&amp;rsquo;re thinking about your firewall daily, something&amp;rsquo;s wrong. Fix it or replace it.&lt;/p&gt;
&lt;p&gt;If you ever decide you want a single box that can run the firewall plus a few other services, hardware with serious NIC density gives you room to grow without a second appliance.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
Nice to have but not required. With multiple 10GbE and 2.5GbE ports, this mini workstation gives you flexible, high-performance hardware for running OPNsense or pfSense as part of a multi-role homelab node. Overkill for a firewall-only setup.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="faqs-opnsense-vs-pfsense-for-homelabs"&gt;FAQs: OPNsense vs pfSense for Homelabs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the easiest firewall for a homelab?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;OPNsense. Better UI, fewer required plugins. You&amp;rsquo;ll spend less time hunting through menus.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does OPNsense run better on low-end hardware than pfSense?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No meaningful difference. Hardware quality matters more. A good Intel NIC beats a bad Realtek NIC regardless of firewall choice.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why do pfSense updates feel delayed?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;pfSense CE trails pfSense Plus. Netgate prioritizes Plus for paying customers. CE gets updates eventually. You wait.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I set up WireGuard without plugins?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;pfSense requires installing the plugin. OPNsense ships with it by default.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is OPNsense&amp;#39;s IDPS as good as pfSense &amp;#43; Suricata?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;They both use Suricata. Same engine, same rules, same detection. OPNsense integrates it cleaner.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ pfSense menu is confusing, how does OPNsense compare?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;OPNsense uses a left-side menu with clearer grouping. Things are where you&amp;rsquo;d expect them. Not revolutionary, but noticeably better.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I trust Netgate long-term?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Netgate is a for-profit company. They need revenue. OPNsense is community-driven. If the CE/Plus split bothers you now, it&amp;rsquo;ll only get worse. Pick accordingly.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Best plugins for homelab monitoring?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;ntopng on pfSense for traffic analysis. Zenarmor on OPNsense for deep packet inspection. Both work well.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I migrate from pfSense to OPNsense?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Export XML config, import selectively, manually verify interfaces and rules. Plan for manual reconfiguration. Not one-click, but doable in an afternoon. (Make a backup first. Obviously.)&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-pick-your-homelab-firewall-and-move-on"&gt;Conclusion: Pick Your Homelab Firewall and Move On
&lt;/h2&gt;&lt;p&gt;In 2026, choosing between OPNsense and pfSense isn&amp;rsquo;t about raw capability. It&amp;rsquo;s about philosophy, workflow, and trust.&lt;/p&gt;
&lt;p&gt;pfSense is powerful, stable, and widely documented. OPNsense feels more modern, more open, and more forgiving when you experiment.&lt;/p&gt;
&lt;p&gt;I want my firewall to fade into the background. For me, that&amp;rsquo;s OPNsense. For you, it might be pfSense.&lt;/p&gt;
&lt;p&gt;Pick one. Document your setup. Back up before upgrades. Get back to building the fun parts of your homelab.&lt;/p&gt;
&lt;p&gt;Your firewall should be boring. That&amp;rsquo;s the whole point.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://diymediaserver.com/images/logos/logo-opnsense.svg" alt="OPNsense Official Documentation" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://docs.opnsense.org/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/pfSense_hu_bcecc21bf81f185c.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/pfSense_hu_216871db39cb7c3e.webp" width="100" height="100" alt="pfSense Official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://docs.netgate.com/pfsense/en/latest/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How to Install, Configure, and Migrate to a Consolidated PostgreSQL Server</title><link>https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/</link><pubDate>Sat, 17 Jan 2026 06:48:12 -0700</pubDate><guid>https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/featured_hu_1a767078bca2b203.webp" alt="Featured image of post How to Install, Configure, and Migrate to a Consolidated PostgreSQL Server" /&gt;&lt;p&gt;If you&amp;rsquo;re running more than a couple of Docker stacks, chances are you&amp;rsquo;re also running more PostgreSQL containers than you want to admit. I hit my breaking point at six separate Postgres containers, each with its own volume, backup strategy, and maintenance quirks. Every time I ran: &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; it felt risky.&lt;/p&gt;
&lt;p&gt;Consolidating PostgreSQL databases into a single dedicated server eliminates scattered backups, reduces resource bloat, and makes future migrations trivial, which is exactly what you want from a database. In this guide, you&amp;rsquo;ll learn how to install PostgreSQL on Debian, configure remote database access, and migrate Docker PostgreSQL containers to a centralized VM without breaking your apps or losing data.&lt;/p&gt;
&lt;p&gt;This PostgreSQL migration guide moves you from multiple Docker-based instances to one dedicated server running in a Debian 13 VM on Proxmox. Expected downtime: 10-30 minutes per app during migration. Risk level: Medium, but I&amp;rsquo;ll also cover recovery strategies.&lt;/p&gt;
&lt;p&gt;This is written for users who already understand Docker, basic Linux administration, and Postgres fundamentals, but want a clean, repeatable way to consolidate databases.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Create a Debian 13 VM in Proxmox, install PostgreSQL natively, enable secure remote access, migrate each Docker database using &lt;code&gt;pg_dump&lt;/code&gt;, then update your Docker stacks to point at the new centralized server for simpler management and backups.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-a-dedicated-postgresql-vm-beats-multiple-containers"&gt;Why a Dedicated PostgreSQL VM Beats Multiple Containers
&lt;/h2&gt;&lt;p&gt;Before running any commands, let&amp;rsquo;s talk about why this is worth the effort.&lt;/p&gt;
&lt;p&gt;When each Docker stack runs its own Postgres container, you&amp;rsquo;re dealing with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Duplicated memory and CPU usage across every instance&lt;/li&gt;
&lt;li&gt;Backups scattered across volumes (good luck finding the right one when you need it)&lt;/li&gt;
&lt;li&gt;Version upgrades happening at different times (or not at all)&lt;/li&gt;
&lt;li&gt;Monitoring and tuning that&amp;rsquo;s inconsistent at best&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A single PostgreSQL server in a VM gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One place to back up and restore, your future self will thank you&lt;/li&gt;
&lt;li&gt;Centralized performance tuning that matters&lt;/li&gt;
&lt;li&gt;Cleaner Docker stacks with fewer moving parts&lt;/li&gt;
&lt;li&gt;Easier upgrades and security patching (do it once, not six times)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s the thing: databases need to be treated as infrastructure, not app dependencies. Treating Postgres like shared infrastructure pays off quickly. I wish I&amp;rsquo;d done this years ago.&lt;/p&gt;
&lt;p&gt;More details on why you should do this:
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/featured_hu_243dd1cd0d4a98aa.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/featured_hu_19f1ffc9ca778244.webp" width="100" height="100" alt="My post on why you Should consolidate your PostgreSQL Docker Containers in to on consolidated VM" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;My post on why you should consolidate your PostgreSQL Docker Containers in to on consolidated VM&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;h2 id="prepare-the-proxmox-vm-debian-13"&gt;Prepare the Proxmox VM (Debian 13)
&lt;/h2&gt;&lt;h3 id="vm-sizing-and-creation"&gt;VM Sizing and Creation
&lt;/h3&gt;&lt;p&gt;In Proxmox, create a new VM with these baseline specs. These specs work well for 5 to 10 typical homelab apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; OS: Debian 13 ISO&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; CPU: 2 to 4 cores&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; RAM: 4 to 8 GB&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Disk: 20 to 50 GB SSD, VirtIO&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Network: Bridge to your homelab subnet&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Enable QEMU Guest Agent in VM options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL benefits more from RAM and fast storage than raw CPU. If you have the memory to spare, err on the side of more, you won&amp;rsquo;t regret it.&lt;/p&gt;
&lt;h3 id="install-debian-13-minimal"&gt;Install Debian 13 (Minimal)
&lt;/h3&gt;&lt;p&gt;During the Debian installer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose a minimal install, no desktop environment (you don&amp;rsquo;t need it)&lt;/li&gt;
&lt;li&gt;Assign a static IP, saves headaches later&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;openssh-server&lt;/code&gt;, so you can manage the VM remotely with SSH&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After the first login, update the system and install the QEMU guest agent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y qemu-guest-agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now qemu-guest-agent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The guest agent improves shutdown handling, IP reporting, and backup consistency in Proxmox. It&amp;rsquo;s not required, but it&amp;rsquo;s one of those &amp;ldquo;install it now, appreciate it later&amp;rdquo; things.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;&lt;br&gt;
This compact mini-workstation offers powerful CPU options and serious I/O, making it ideal for running a consolidated PostgreSQL server in a Proxmox VM with room for future growth.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="install-postgresql-on-debian-13"&gt;Install PostgreSQL on Debian 13
&lt;/h2&gt;&lt;h3 id="decision-default-debian-repo-or-official-postgresql-repo"&gt;Decision: Default Debian Repo or Official PostgreSQL Repo?
&lt;/h3&gt;&lt;p&gt;Alright, this is the first real fork in the road. Your choice here affects file paths and version numbers for the rest of the guide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option A: Debian default repository (RECOMMENDED)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pros: simpler, stable, fewer surprises&lt;/li&gt;
&lt;li&gt;Cons: version may lag behind upstream (Debian 13 uses PostgreSQL 17)&lt;/li&gt;
&lt;li&gt;Install paths: &lt;code&gt;/etc/postgresql/17/main/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service name: &lt;code&gt;postgresql&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y postgresql postgresql-contrib
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Option B: Official PostgreSQL APT repository&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pros: access to PostgreSQL 18 or newer features&lt;/li&gt;
&lt;li&gt;Cons: slightly more setup complexity, one more thing to maintain&lt;/li&gt;
&lt;li&gt;Install paths: &lt;code&gt;/etc/postgresql/18/main/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Service name: &lt;code&gt;postgresql&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want the latest features:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y curl ca-certificates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -m &lt;span class="m"&gt;0755&lt;/span&gt; -d /etc/apt/keyrings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -o /etc/apt/keyrings/pgdg.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod a+r /etc/apt/keyrings/pgdg.asc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [signed-by=/etc/apt/keyrings/pgdg.asc] https://apt.postgresql.org/pub/repos/apt &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release -cs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;-pgdg main&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/pgdg.list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y postgresql-18 postgresql-contrib-18
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
The rest of this guide assumes Debian 13&amp;rsquo;s default PostgreSQL 17. If you chose PGDG, replace &lt;code&gt;/etc/postgresql/17/main/&lt;/code&gt; with &lt;code&gt;/etc/postgresql/18/main/&lt;/code&gt; in all config file paths. I know, I know, it&amp;rsquo;s annoying, but that&amp;rsquo;s the price you pay for bleeding edge.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="verify-the-service"&gt;Verify the Service
&lt;/h3&gt;&lt;p&gt;PostgreSQL starts automatically after installation. Let&amp;rsquo;s make sure it&amp;rsquo;s actually running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status postgresql
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; postgresql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see &amp;ldquo;active (running)&amp;rdquo;, you&amp;rsquo;re golden. Now access the admin shell:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -i -u postgres psql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see the &lt;code&gt;postgres=#&lt;/code&gt; prompt. Check your version with &lt;code&gt;SELECT version();&lt;/code&gt;&lt;br&gt;
Output should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-------------------------------------------------------------------------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PostgreSQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;deb13u1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x86_64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;linux&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gnu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;compiled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gcc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then exit with &lt;code&gt;\q&lt;/code&gt;.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;&lt;br&gt;
Its multiple NVMe slots and dual 10G SFP+ networking make it a practical, quiet, and efficient choice for hosting a dedicated PostgreSQL server and handling VM workloads.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="configure-postgresql-for-remote-access"&gt;Configure PostgreSQL for Remote Access
&lt;/h2&gt;&lt;p&gt;By default, PostgreSQL only listens on localhost. That&amp;rsquo;s good for security, but useless for Docker containers on other hosts. Let&amp;rsquo;s fix that.&lt;/p&gt;
&lt;h3 id="enable-network-listening"&gt;Enable Network Listening
&lt;/h3&gt;&lt;p&gt;Edit &lt;code&gt;postgresql.conf&lt;/code&gt; (adjust path for your version):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/postgresql/17/main/postgresql.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Find and set:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;listen_addresses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells PostgreSQL to listen on all interfaces. Don&amp;rsquo;t panic. Access control is handled separately. You&amp;rsquo;re not opening the floodgates here.&lt;/p&gt;
&lt;h3 id="configure-client-authentication"&gt;Configure Client Authentication
&lt;/h3&gt;&lt;p&gt;Edit &lt;code&gt;pg_hba.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/postgresql/17/main/pg_hba.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add a rule for your Docker subnet(s) and your LAN. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# LAN Subnet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;host all all 192.168.1.0/24 scram-sha-256&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Docker 1 Subnet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;host all all 172.17.0.0/16 scram-sha-256&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Docker 2 Subnet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;host all all 172.18.0.0/16 scram-sha-256&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Use &lt;code&gt;scram-sha-256&lt;/code&gt; authentication instead of &lt;code&gt;md5&lt;/code&gt; where possible. It&amp;rsquo;s more secure and the modern default. Your Docker clients will handle it fine.&lt;/p&gt;
&lt;p&gt;Restart PostgreSQL:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart postgresql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="firewall-the-database"&gt;Firewall the Database
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Never&lt;/em&gt;&lt;/strong&gt; expose PostgreSQL to the internet. Don&amp;rsquo;t even think about it. Lock it down to your trusted subnets.&lt;/p&gt;
&lt;p&gt;Using UFW:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y ufw
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow 22/tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 192.168.1.0/24 to any port &lt;span class="m"&gt;5432&lt;/span&gt; proto tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 172.17.0.0/16 to any port &lt;span class="m"&gt;5432&lt;/span&gt; proto tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 172.18.0.0/16 to any port &lt;span class="m"&gt;5432&lt;/span&gt; proto tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify it is working:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Adjust the subnets to match your LAN, Docker hosts, and Proxmox bridge. If you&amp;rsquo;re not sure what subnet to use, check your Docker network with &lt;code&gt;docker network inspect bridge&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It should look like this when you are done:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Status: active
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;To Action From
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-- ------ ----
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5432/tcp ALLOW 192.168.1.0/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;22/tcp ALLOW Anywhere
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5432/tcp ALLOW 172.18.0.0/16
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5432/tcp ALLOW 172.17.0.0/16
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Checkpoint&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;From another host or container already running Postgres, test connectivity.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it &amp;lt;container_name&amp;gt; /bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run this to test if the firewall is allowing traffic on this subnet&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -h &amp;lt;vm_ip&amp;gt; -U postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it connects and asks for a password, you&amp;rsquo;re good.&lt;br&gt;
Press &lt;code&gt;ctrl-c&lt;/code&gt; to abort the connection and &lt;code&gt;exit&lt;/code&gt; to leave the container.&lt;/p&gt;
&lt;p&gt;If it fails, check logs with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u postgresql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nine times out of ten, it&amp;rsquo;s either &lt;code&gt;pg_hba.conf&lt;/code&gt; or the firewall. Double-check both.&lt;/p&gt;
&lt;h2 id="create-users-and-databases-for-docker-apps"&gt;Create Users and Databases for Docker Apps
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where a lot of people mess up: they dump everything into a single database with a single superuser. Resist that urge.&lt;/p&gt;
&lt;h3 id="identify-existing-containers"&gt;Identify Existing Containers
&lt;/h3&gt;&lt;p&gt;On your Docker host(s):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps &lt;span class="p"&gt;|&lt;/span&gt; grep postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make a list of containers, databases, and users. Seriously, write it down or put it in a text file. You&amp;rsquo;ll reference it constantly.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
I recommend that you DO NOT consolidate the Immich Postgres database. They use older versions of Postgres and Postgres extensions. I tried to do this and wasted quite a few hours, and then I realized that if I got it working it would be a pain to update in the future. Leave it in its docker container unless you REALLY know what you are doing.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Create a dedicated role and database per application. It makes troubleshooting and permissions way simpler later.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -i -u postgres psql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ROLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ExampleUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LOGIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;StrongPasswordHere&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;example_db&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OWNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ExampleUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Repeat this for each former Docker-based Postgres instance. Yeah, it&amp;rsquo;s a bit tedious, but you only do it once.&lt;/p&gt;
&lt;p&gt;Test access:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -h localhost -U ExampleUser -d example_db
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see the &lt;code&gt;example_db=&amp;gt;&lt;/code&gt; prompt, you&amp;rsquo;re set. Exit with &lt;code&gt;\q&lt;/code&gt;.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Matching users and database names to applications (like &lt;code&gt;example&lt;/code&gt; and &lt;code&gt;example_db&lt;/code&gt;) makes your life so much easier six months from now when you&amp;rsquo;re trying to remember which database belongs to what.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="migrate-docker-postgresql-databases-to-dedicated-server"&gt;Migrate Docker PostgreSQL Databases to Dedicated Server
&lt;/h2&gt;&lt;p&gt;Alright, this is the heart of the PostgreSQL migration process. Take your time here, rushing this step is how you end up restoring from backups (You have backups right? RIGHT?).&lt;/p&gt;
&lt;h3 id="handle-extensions-and-ownership-issues-first"&gt;Handle Extensions and Ownership Issues First
&lt;/h3&gt;&lt;p&gt;Before dumping, identify potential problems. This saves you from &amp;ldquo;why won&amp;rsquo;t it restore?&amp;rdquo; headaches later:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check for extensions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it &amp;lt;container&amp;gt; psql -U &amp;lt;user&amp;gt; -d &amp;lt;db&amp;gt; -c &lt;span class="s2"&gt;&amp;#34;\dx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check for custom roles&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it &amp;lt;container&amp;gt; psql -U &amp;lt;user&amp;gt; -c &lt;span class="s2"&gt;&amp;#34;\du&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Common issues you&amp;rsquo;ll hit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extensions like &lt;code&gt;pg_trgm&lt;/code&gt; or &lt;code&gt;uuid-ossp&lt;/code&gt; may need manual creation on the target server
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXTENSION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uuid-ossp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Role ownership conflicts between different container dumps (especially if you&amp;rsquo;re using &lt;code&gt;pg_dumpall&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="dump-the-data"&gt;Dump the Data
&lt;/h3&gt;&lt;p&gt;Decision point here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small homelab DBs (under a few GB): dumping while containers are running is usually fine&lt;/li&gt;
&lt;li&gt;Larger or critical DBs: stop the app container first to guarantee consistency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For single database (recommended):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it &amp;lt;container&amp;gt; pg_dump -h 127.0.0.1 -U postgres -d example_db --format&lt;span class="o"&gt;=&lt;/span&gt;plain --no-owner --no-privileges &amp;gt; example_dump.sql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;--no-owner --no-privileges&lt;/code&gt; flags avoid role and ownership conflicts during restore. You&amp;rsquo;ll set ownership manually on the new server anyway.&lt;/p&gt;
&lt;p&gt;For all databases and roles (advanced, usually overkill for homelabs):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -i &amp;lt;container&amp;gt; pg_dumpall -h 127.0.0.1 -U &amp;lt;user&amp;gt; &amp;gt; full_dump.sql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Watch the output. If you see errors about missing permissions or roles, that&amp;rsquo;s your cue to use &lt;code&gt;--no-owner --no-privileges&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="transfer-dumps-to-the-vm"&gt;Transfer Dumps to the VM
&lt;/h3&gt;&lt;p&gt;Use &lt;code&gt;scp&lt;/code&gt; or VSCode to download and upload to the new server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp db_dump.sql user@postgres-vm:/tmp/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you&amp;rsquo;ve got multiple dumps, throw them all in &lt;code&gt;/tmp/&lt;/code&gt; on the VM. Remember to clean them up later.&lt;/p&gt;
&lt;h3 id="restore-into-the-new-server"&gt;Restore into the New Server
&lt;/h3&gt;&lt;p&gt;On the PostgreSQL VM:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -h localhost -U ExampleUser -d example_db -f /tmp/db_dump.sql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Repeat for each application. Yeah, it&amp;rsquo;s repetitive. Put on some music.&lt;/p&gt;
&lt;p&gt;If you hit extension errors like &amp;ldquo;extension uuid-ossp does not exist&amp;rdquo;, install them manually:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXTENSION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uuid-ossp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Most common extensions you&amp;rsquo;ll need: &lt;code&gt;uuid-ossp&lt;/code&gt;, &lt;code&gt;pg_trgm&lt;/code&gt;, &lt;code&gt;hstore&lt;/code&gt;. Install them as needed.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DX2DPJZ5"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NVME-2TB_hu_4dac0086f1363f66.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NVME-2TB_hu_fb4af511003be25d.webp" width="600" height="600" alt="Samsung NVMe 9100 PRO 2TB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;The Samsung 9100 PRO 2TB&lt;/strong&gt;&lt;br&gt;
Great choice for a PostgreSQL database because its very high IOPS and low latency keep reads and writes snappy under load, while the sustained throughput helps with WAL logging, indexes, and vacuum/maintenance jobs without the whole system feeling sluggish.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3YMvopG" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="update-docker-stacks-to-use-the-central-database"&gt;Update Docker Stacks to Use the Central Database
&lt;/h2&gt;&lt;p&gt;This step removes PostgreSQL from your Docker stacks entirely. It&amp;rsquo;s weirdly satisfying.&lt;/p&gt;
&lt;p&gt;In your &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Comment out the &lt;code&gt;postgres&lt;/code&gt; service (Remove once you know it works)&lt;/li&gt;
&lt;li&gt;Comment out volume definitions tied to Postgres (Remove once you know it works)&lt;/li&gt;
&lt;li&gt;Update environment variables to point at the new server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;POSTGRES_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;&amp;lt;postgres_VM_IP&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;example_db&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ExampleUser&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;StrongPasswordHere&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Recreate the stack:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your application should now connect to the centralized PostgreSQL server. Check the logs to make sure it&amp;rsquo;s connecting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose logs -f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see database connection errors, double-check your environment variables. Typos in the hostname or database name trip up 80% of people here.&lt;/p&gt;
&lt;p&gt;Once you do this for one stack, the rest should be faster.&lt;/p&gt;
&lt;h2 id="postgresql-performance-tuning-for-homelabs"&gt;PostgreSQL Performance Tuning for Homelabs
&lt;/h2&gt;&lt;p&gt;Out of the box PostgreSQL settings are conservative. Here&amp;rsquo;s safe starting points for a homelab:&lt;/p&gt;
&lt;p&gt;Edit &lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-conf" data-lang="conf"&gt;shared_buffers = 1GB
work_mem = 16MB
max_connections = 100
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Why these settings matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_buffers&lt;/code&gt;: PostgreSQL&amp;rsquo;s main cache. Rule of thumb is 25% of system RAM, but too much can actually hurt performance on smaller systems. For a 4GB VM, 1GB is the sweet spot.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;work_mem&lt;/code&gt;: Per-operation memory for sorts and joins. This multiplies by concurrent connections, so be conservative. 16MB is safe for most homelabs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_connections&lt;/code&gt;: Many apps hold idle connections. 100 is safe for most homelabs, you&amp;rsquo;re probably not hitting 50 concurrent connections anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Don&amp;rsquo;t blindly scale these up. A 4GB VM with &lt;code&gt;work_mem = 64MB&lt;/code&gt; and 100 connections could theoretically use 6.4GB of RAM during heavy queries. This is how you OUT OF MEMORY (OOM) kill your databases.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Restart PostgreSQL after changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart postgresql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These settings work well for read-heavy media workloads. If you&amp;rsquo;re running something write-heavy, you&amp;rsquo;ll want to tune further, but this is a good baseline.&lt;/p&gt;
&lt;h2 id="troubleshooting-postgresql-migration-issues"&gt;Troubleshooting PostgreSQL Migration Issues
&lt;/h2&gt;&lt;h3 id="connection-refused-from-docker-containers"&gt;Connection Refused from Docker Containers
&lt;/h3&gt;&lt;p&gt;This is the most common issue. Here&amp;rsquo;s the checklist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirm PostgreSQL is listening: &lt;code&gt;ss -lntp | grep 5432&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;listen_addresses = '*'&lt;/code&gt; is set and not commented out&lt;/li&gt;
&lt;li&gt;Check firewall rules with &lt;code&gt;sudo ufw status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Double-check &lt;code&gt;pg_hba.conf&lt;/code&gt; subnet and authentication method&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re still stuck, check the logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u postgresql -n &lt;span class="m"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for &amp;ldquo;connection refused&amp;rdquo; or &amp;ldquo;no pg_hba.conf entry&amp;rdquo; messages. They&amp;rsquo;ll tell you exactly what&amp;rsquo;s wrong.&lt;/p&gt;
&lt;h3 id="pg_dump-fails-with-role-does-not-exist"&gt;pg_dump Fails with &amp;ldquo;Role Does Not Exist&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;This usually means roles from the old container don&amp;rsquo;t exist yet on the new server.&lt;/p&gt;
&lt;p&gt;Fix by either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating the roles first with &lt;code&gt;CREATE ROLE&lt;/code&gt;, or&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;--no-owner --no-privileges&lt;/code&gt; during dump and restore (recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using &lt;code&gt;--no-owner --no-privileges&lt;/code&gt; is cleaner. You&amp;rsquo;re not trying to preserve complex permission structures. You want the data.&lt;/p&gt;
&lt;h3 id="authentication-errors-after-migration"&gt;Authentication Errors After Migration
&lt;/h3&gt;&lt;p&gt;Frustrating, I know. Check these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure passwords match what&amp;rsquo;s in Docker environment variables (no trailing spaces)&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;scram-sha-256&lt;/code&gt; is supported by your client library (it should be)&lt;/li&gt;
&lt;li&gt;Check for copy-paste errors in passwords, seriously, this happens more than you&amp;rsquo;d think&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re still stuck, temporarily switch to &lt;code&gt;md5&lt;/code&gt; in &lt;code&gt;pg_hba.conf&lt;/code&gt; to isolate the issue. If that works, it&amp;rsquo;s a &lt;code&gt;scram-sha-256&lt;/code&gt; compatibility problem.&lt;/p&gt;
&lt;h3 id="performance-worse-than-before"&gt;Performance Worse Than Before
&lt;/h3&gt;&lt;p&gt;Wait, what? Yeah, this can happen. Here&amp;rsquo;s why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your containers may have been memory-starved without you realizing it, so they were &amp;ldquo;fast&amp;rdquo; because they weren&amp;rsquo;t doing much&lt;/li&gt;
&lt;li&gt;The new server is doing proper caching and query planning&lt;/li&gt;
&lt;li&gt;You need to increase VM RAM or revisit &lt;code&gt;shared_buffers&lt;/code&gt; and &lt;code&gt;work_mem&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; on slow queries to see what&amp;rsquo;s happening. Usually it&amp;rsquo;s a matter of giving PostgreSQL more memory to work with.&lt;/p&gt;
&lt;h3 id="extension-errors-during-restore"&gt;Extension Errors During Restore
&lt;/h3&gt;&lt;p&gt;If you see &amp;ldquo;extension does not exist&amp;rdquo; errors, install them manually:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXTENSION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uuid-ossp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EXTENSION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;EXISTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pg_trgm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Most homelab apps use a handful of common extensions. Install them once and you&amp;rsquo;re done.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I connect to PostgreSQL from Docker containers on a different host?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Expose PostgreSQL on the VM&amp;rsquo;s IP, allow the Docker subnet in &lt;code&gt;pg_hba.conf&lt;/code&gt;, and ensure your firewall permits port 5432 only from trusted subnets. Never open it to the internet, really, don&amp;rsquo;t do it.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the safest way to migrate a large Docker PostgreSQL database without downtime?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For homelabs, brief downtime is safest. Stop the app, run &lt;code&gt;pg_dump&lt;/code&gt;, restore, then restart the app pointing at the new server. Trying to do zero-downtime migrations in a homelab is usually more trouble than it&amp;rsquo;s worth.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why can&amp;#39;t I access PostgreSQL after install?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;By default, PostgreSQL listens only on localhost. You must update &lt;code&gt;listen_addresses&lt;/code&gt; and &lt;code&gt;pg_hba.conf&lt;/code&gt;, then restart the service. This trips up everyone the first time.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use Debian repos or the official PostgreSQL repo?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Debian repos prioritize stability. Official repos give newer versions. For most homelabs, Debian&amp;rsquo;s version is the safer default. Unless you need a specific PostgreSQL 18 feature, stick with Debian&amp;rsquo;s package.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How much RAM and CPU should I allocate to a PostgreSQL VM?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For 5 to 10 small apps, 2 to 4 CPU cores and 4 to 8 GB RAM is sufficient. PostgreSQL benefits more from RAM than CPU. If you&amp;rsquo;re running out of resources, add RAM first.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I migrate Docker PostgreSQL databases while containers are running?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, for small databases (under a few GB). For consistency, stopping the app container is safer. The downtime is usually under 10 minutes, your users might not even notice.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the QEMU guest agent used for?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;It improves Proxmox integration, clean shutdowns, and backup behavior. It&amp;rsquo;s not mandatory but recommended. Install it now, and thank yourself later when you&amp;rsquo;re doing VM backups.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Consolidating multiple Docker-based PostgreSQL instances into a single dedicated server is one of those changes that feels intimidating but pays off immediately. You reduce clutter, simplify backups, and gain real control over your data layer.&lt;/p&gt;
&lt;p&gt;The key takeaways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a dedicated Debian 13 VM for PostgreSQL, treat it as one of your most important VMs&lt;/li&gt;
&lt;li&gt;Keep one user and database per application (makes troubleshooting way easier)&lt;/li&gt;
&lt;li&gt;Migrate with &lt;code&gt;pg_dump --no-owner --no-privileges&lt;/code&gt;, not filesystem copies&lt;/li&gt;
&lt;li&gt;Lock down network access to trusted subnets only&lt;/li&gt;
&lt;li&gt;Handle extensions and roles proactively during migration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re tired of babysitting half a dozen Postgres containers like I was, this approach will make your homelab cleaner and more predictable.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/postgresql_hu_9f46bcd831d480b3.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/postgresql_hu_43ed92609cc4bb42.webp" width="100" height="100" alt="PostgreSQL Official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://www.postgresql.org/docs/current/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/debian_hu_78b696a2ac04e9b9.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/debian_hu_b470da81290719d4.webp" width="100" height="100" alt="Debian PostgreSQL Wiki" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Debian PostgreSQL Wiki&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://wiki.debian.org/PostgreSQL"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/Proxmox_hu_3ceabd90f199854d.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/Proxmox_hu_51a1a565d5e57244.webp" width="100" height="100" alt="Proxmox VE Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Proxmox VE Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://pve.proxmox.com/wiki/Main_Page"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;&lt;br&gt;
This full-tower case is useful if you want a quiet, storage-heavy homelab server for Proxmox, but may be overkill for smaller or more compact builds.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Should You Consolidate PostgreSQL Databases In One VM</title><link>https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/</link><pubDate>Fri, 16 Jan 2026 07:39:34 -0700</pubDate><guid>https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/consolidate-postgresql-databases-one-vm/featured_hu_38e9f95c061b041a.webp" alt="Featured image of post Should You Consolidate PostgreSQL Databases In One VM" /&gt;&lt;h2 id="why-consolidate-multiple-postgresql-databases-into-one-vm"&gt;Why Consolidate Multiple PostgreSQL Databases Into One VM
&lt;/h2&gt;&lt;p&gt;If your homelab&amp;rsquo;s quietly accumulated half a dozen or more Postgres containers, one per Docker stack, you&amp;rsquo;re not alone. This is how it always starts: an app needs a database, the compose file spins one up, and you move on. Fast-forward a year or two, and you&amp;rsquo;re patching several Postgres containers, backing up all the separate volumes, and troubleshooting six or more slightly different configurations.&lt;/p&gt;
&lt;p&gt;Move them onto a single Postgres VM and the operational tax drops fast. One backup job. One upgrade window. One place to tune. If you&amp;rsquo;re running 3+ Postgres databases across Home Assistant, Wiki.js, Immich, and a monitoring stack, that&amp;rsquo;s hours a month you stop spending on babysitting containers.&lt;/p&gt;
&lt;p&gt;I hit that wall myself. Managing six separate PostgreSQL containers felt like busywork instead of actual homelabbing. Consolidating them into a single Postgres VM changed that overnight. Fewer moving parts, simpler backups, and one place to tune database performance.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the thing: consolidating your Postgres databases onto one VM is usually the better call for intermediate homelabs. I&amp;rsquo;ll walk through why it simplifies management and backups, and the cases where you absolutely shouldn&amp;rsquo;t do it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who should consolidate:&lt;/strong&gt; Homelabbers running 3+ Postgres containers with low to moderate traffic workloads like Home Assistant, Wiki.js, and monitoring stacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who should not:&lt;/strong&gt; Those with high-write databases, strict isolation requirements, or apps that need different PostgreSQL versions.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Running all your Postgres databases on one dedicated Postgres VM reduces sprawl, simplifies backups, and cuts management overhead, as long as your workloads are modest, and you plan for resource contention.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-consolidating-postgresql-databases-matters"&gt;Why Consolidating PostgreSQL Databases Matters
&lt;/h2&gt;&lt;p&gt;The hidden cost of &amp;ldquo;one database per stack&amp;rdquo; isn&amp;rsquo;t CPU or RAM. It&amp;rsquo;s operational overhead.&lt;/p&gt;
&lt;p&gt;Every Postgres container means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its own data volume&lt;/li&gt;
&lt;li&gt;Its own backup job (or worse, no backup at all)&lt;/li&gt;
&lt;li&gt;Its own upgrade and patch cycle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From an infrastructure perspective, this is wasteful. PostgreSQL&amp;rsquo;s shared buffer cache, background workers, and WAL processes all duplicate across instances. A single instance hosting multiple databases eliminates this duplication. In my own setup, memory usage dropped from 3.2GB across six containers to 1.8GB with one consolidated instance.&lt;/p&gt;
&lt;p&gt;For a homelab where workloads are rarely extreme, consolidation&amp;rsquo;s usually a net win.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/featured_hu_1186bac5d06eaf27.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/featured_hu_45690bd138c4ff33.webp" width="100" height="100" alt="My post on how-to consolidate your PostgreSQL Docker Containers in to on consolidated VM." loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;My post on how-to consolidate your PostgreSQL Docker Containers in to on consolidated VM.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/consolidate-postgresql-server-docker-migration/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="my-story-death-by-a-thousand-containers"&gt;My Story: Death by a Thousand Containers
&lt;/h2&gt;&lt;p&gt;At one point, I was running six separate PostgreSQL containers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Home Assistant&lt;/li&gt;
&lt;li&gt;Wiki.js&lt;/li&gt;
&lt;li&gt;Immich&lt;/li&gt;
&lt;li&gt;A monitoring stack&lt;/li&gt;
&lt;li&gt;Two side projects I barely touched&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each had its own compose file and volume. When it came time to back them up, I had six cron jobs dumping databases in slightly different ways. When PostgreSQL 13 went end-of-life, I had to plan multiple upgrades.&lt;/p&gt;
&lt;p&gt;After consolidating everything into one Postgres VM, backups became a single script, upgrades happened once, and adding a new database was a 30-second task instead of its own mini-project.&lt;/p&gt;
&lt;h2 id="one-postgres-vm-vs-one-postgres-container-per-stack"&gt;One Postgres VM vs One Postgres Container Per Stack
&lt;/h2&gt;&lt;h3 id="the-container-per-app-model"&gt;The Container-Per-App Model
&lt;/h3&gt;&lt;p&gt;This model&amp;rsquo;s popular because it&amp;rsquo;s easy to start with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drop a &lt;code&gt;postgres:&lt;/code&gt; image into your compose file&lt;/li&gt;
&lt;li&gt;Link it to your app&lt;/li&gt;
&lt;li&gt;Forget about it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Strong isolation boundaries&lt;/li&gt;
&lt;li&gt;Easy to reason about for beginners&lt;/li&gt;
&lt;li&gt;App and database lifecycles are tightly coupled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fragmented backups&lt;/li&gt;
&lt;li&gt;Repeated configuration and tuning&lt;/li&gt;
&lt;li&gt;Higher memory and disk overhead&lt;/li&gt;
&lt;li&gt;More patching and monitoring work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach scales poorly as your homelab grows.&lt;/p&gt;
&lt;h3 id="the-dedicated-postgres-vm-model"&gt;The Dedicated Postgres VM Model
&lt;/h3&gt;&lt;p&gt;In this model, you run one PostgreSQL instance on a dedicated Postgres VM. Each app gets its own database and role inside that instance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Centralized management and upgrades&lt;/li&gt;
&lt;li&gt;One backup strategy for all databases&lt;/li&gt;
&lt;li&gt;Better overall resource utilization&lt;/li&gt;
&lt;li&gt;Easier monitoring and performance tuning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Less isolation than separate containers&lt;/li&gt;
&lt;li&gt;Risk of resource contention if poorly sized&lt;/li&gt;
&lt;li&gt;Requires more up-front planning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most intermediate homelabs, the pros outweigh the cons. You&amp;rsquo;re trading a bit of isolation for dramatically simpler operations.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
&lt;strong&gt;Must have for this build.&lt;/strong&gt;
Why it fits this post: Its high core count, fast networking (dual 10GbE), and flexible storage make it ideal for running a consolidated PostgreSQL VM with room for future database growth and multiple homelab workloads.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-a-vm-instead-of-lxc-or-more-containers"&gt;Why a VM Instead of LXC or More Containers
&lt;/h2&gt;&lt;h3 id="postgres-vm-vs-lxc-container"&gt;Postgres VM vs LXC Container
&lt;/h3&gt;&lt;p&gt;Running PostgreSQL in LXC containers is tempting. They&amp;rsquo;re lightweight and fast. But for databases, the trade-offs matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VM advantages for PostgreSQL:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Better I/O isolation under load&lt;/li&gt;
&lt;li&gt;Cleaner snapshot and backup integration with hypervisors like Proxmox&lt;/li&gt;
&lt;li&gt;Fewer surprises from shared kernel behavior&lt;/li&gt;
&lt;li&gt;Predictable fsync behavior with dedicated virtual disks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;LXC advantages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lower RAM overhead (200-500MB saved)&lt;/li&gt;
&lt;li&gt;Faster startup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Proxmox-specific considerations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use VirtIO SCSI with &amp;ldquo;Write back&amp;rdquo; cache for VM disk performance&lt;/li&gt;
&lt;li&gt;If you&amp;rsquo;re on ZFS, disable sync writes for the VM dataset to avoid double-fsync&lt;/li&gt;
&lt;li&gt;For LXC, unprivileged containers require proper UID/GID mapping for Postgres&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s the thing: PostgreSQL&amp;rsquo;s sensitive to disk latency and I/O jitter. A VM gives you more predictable behavior, especially when multiple databases share the same instance. For a dedicated Postgres VM, this predictability&amp;rsquo;s usually worth the small overhead.&lt;/p&gt;
&lt;h2 id="how-consolidating-postgresql-databases-simplifies-backups"&gt;How Consolidating PostgreSQL Databases Simplifies Backups
&lt;/h2&gt;&lt;p&gt;This is where consolidation really shines.&lt;/p&gt;
&lt;p&gt;With multiple containers, backups often look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different schedules&lt;/li&gt;
&lt;li&gt;Different dump formats&lt;/li&gt;
&lt;li&gt;Different retention policies&lt;/li&gt;
&lt;li&gt;That one database you forgot about entirely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With one Postgres VM, you can choose a single approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logical backups using &lt;code&gt;pg_dump&lt;/code&gt; for each database&lt;/li&gt;
&lt;li&gt;Physical backups using &lt;code&gt;pg_basebackup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Or a full-featured tool like Barman&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL is designed to host multiple databases in one instance. Unified backups aren&amp;rsquo;t a hack. They&amp;rsquo;re the intended path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DATABASES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;homeassistant&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wikijs&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;immich&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grafana&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; db in &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pg_dump -h localhost -U backup_user -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; gzip &amp;gt; &lt;span class="s2"&gt;&amp;#34;/backups/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%Y%m%d&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.sql.gz&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Restores are simpler too. You restore one database, not an entire container volume. When you&amp;rsquo;re staring at a corrupted Wiki.js database, you&amp;rsquo;ll appreciate the difference.&lt;/p&gt;
&lt;h2 id="resource-utilization-and-performance"&gt;Resource Utilization and Performance
&lt;/h2&gt;&lt;h3 id="why-one-instance-is-usually-faster"&gt;Why One Instance Is Usually Faster
&lt;/h3&gt;&lt;p&gt;Every PostgreSQL instance has overhead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Background workers (autovacuum, stats collector)&lt;/li&gt;
&lt;li&gt;Shared buffers (typically 128MB default per instance)&lt;/li&gt;
&lt;li&gt;WAL processes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you run six instances, you pay that cost six times. A single instance with six databases shares those resources way more efficiently.&lt;/p&gt;
&lt;h3 id="the-noisy-neighbor-problem"&gt;The Noisy Neighbor Problem
&lt;/h3&gt;&lt;p&gt;The main risk is one database hogging resources. We&amp;rsquo;ve all been there. Immich decides to index 10,000 photos while Home Assistant is trying to log sensor data, and suddenly your dashboards crawl.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mitigations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Separate roles per app&lt;/li&gt;
&lt;li&gt;Per-database configuration settings&lt;/li&gt;
&lt;li&gt;Connection pooling with PgBouncer&lt;/li&gt;
&lt;li&gt;Monitoring query behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If one workload regularly exceeds 50% of available CPU or I/O, it might deserve its own instance. Consolidation&amp;rsquo;s not all-or-nothing.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C96FR8YC"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="GMKtec Mini PC i7-1185G7" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;GMKtec Mini PC i7-1185G7&lt;/strong&gt;
&lt;strong&gt;Must have for this build.&lt;/strong&gt;
A quad-core i7 with 16 GB of DDR4 and a 1 TB SSD, which is enough headroom to host a dedicated PostgreSQL VM and still leave room for the other services you&amp;rsquo;re consolidating onto the same machine.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/454pyTM" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="should-you-consolidate-your-postgresql-databases"&gt;Should You Consolidate Your PostgreSQL Databases?
&lt;/h2&gt;&lt;p&gt;Use this quick test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your databases are mostly low to medium traffic, consolidate.&lt;/li&gt;
&lt;li&gt;If one database has heavy writes or constant load, consider isolating it.&lt;/li&gt;
&lt;li&gt;If you value simplicity over maximum isolation, consolidate.&lt;/li&gt;
&lt;li&gt;If uptime requirements differ wildly between apps, consider partial consolidation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many homelabs end up with a hybrid approach: one Postgres VM for most apps, and a separate instance for the outlier that&amp;rsquo;s constantly hammering the disk.&lt;/p&gt;
&lt;h2 id="what-homelab-software-benefits-most"&gt;What Homelab Software Benefits Most
&lt;/h2&gt;&lt;p&gt;A dedicated Postgres VM works especially well for apps that already support external databases.&lt;/p&gt;
&lt;p&gt;Common examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Home Assistant&lt;/li&gt;
&lt;li&gt;Wiki.js&lt;/li&gt;
&lt;li&gt;Immich&lt;/li&gt;
&lt;li&gt;Monitoring stacks like Grafana&lt;/li&gt;
&lt;li&gt;Internal tools and dashboards&lt;/li&gt;
&lt;li&gt;n8n&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These apps benefit from stable connections, predictable performance, and easy backups.&lt;/p&gt;
&lt;p&gt;Media servers that use PostgreSQL for metadata also fit well, as long as write rates are reasonable.&lt;/p&gt;
&lt;h2 id="high-level-migration-strategy"&gt;High-Level Migration Strategy
&lt;/h2&gt;&lt;p&gt;At a high level, migration looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inventory your existing databases&lt;/li&gt;
&lt;li&gt;Build a dedicated Postgres VM&lt;/li&gt;
&lt;li&gt;Dump each database&lt;/li&gt;
&lt;li&gt;Restore into the new instance&lt;/li&gt;
&lt;li&gt;Update app connection strings&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is usually downtime-friendly for homelabs, but you can also stage it database by database if you&amp;rsquo;re paranoid about breaking everything at once. (I don&amp;rsquo;t blame you.)&lt;/p&gt;
&lt;h2 id="basic-setup-for-a-postgres-vm"&gt;Basic Setup for a Postgres VM
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;For larger homelabs (5-10 databases):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 to 8 CPU cores&lt;/li&gt;
&lt;li&gt;16 to 32 GB RAM&lt;/li&gt;
&lt;li&gt;SSD or NVMe storage&lt;/li&gt;
&lt;li&gt;PostgreSQL 16 or newer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For smaller homelabs (3-5 databases):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2 to 4 CPU cores&lt;/li&gt;
&lt;li&gt;8 to 16 GB RAM&lt;/li&gt;
&lt;li&gt;SSD storage&lt;/li&gt;
&lt;li&gt;PostgreSQL 16 or newer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Key configuration ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_buffers&lt;/code&gt; around 25% of RAM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;effective_cache_size&lt;/code&gt; around 75% of RAM&lt;/li&gt;
&lt;li&gt;Conservative &lt;code&gt;max_connections&lt;/code&gt; with PgBouncer in front&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These defaults give you room to grow without constant tuning. You can always optimize later when you actually have data showing where the bottlenecks are.&lt;/p&gt;
&lt;h2 id="security-considerations"&gt;Security Considerations
&lt;/h2&gt;&lt;p&gt;Consolidation reduces the number of exposed services, which is good. But isolation now happens at the database level instead of the container level.&lt;/p&gt;
&lt;p&gt;Best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One role per app&lt;/li&gt;
&lt;li&gt;Strong passwords or certificates&lt;/li&gt;
&lt;li&gt;Restrictive &lt;code&gt;pg_hba.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;No shared superuser credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Done right, this isn&amp;rsquo;t less secure than containers. But it does require discipline. Don&amp;rsquo;t get lazy and hand everything the &lt;code&gt;postgres&lt;/code&gt; superuser account because &amp;ldquo;it&amp;rsquo;s only a homelab.&amp;rdquo;&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;
&lt;strong&gt;Nice to have but not required.&lt;/strong&gt;
Why it fits this post: Its compact size, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5GbE) make it a practical, quiet option for a PostgreSQL VM, though with slightly less raw power than the top picks.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-common-postgresql-consolidation-problems"&gt;Troubleshooting Common PostgreSQL Consolidation Problems
&lt;/h2&gt;&lt;h3 id="one-database-is-slowing-everything-down"&gt;One Database Is Slowing Everything Down
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;High CPU or I/O usage&lt;/li&gt;
&lt;li&gt;Other apps feel sluggish&lt;/li&gt;
&lt;li&gt;Your spouse complains that Home Assistant&amp;rsquo;s not responding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fixes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify heavy queries with &lt;code&gt;pg_stat_statements&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Limit connections per app&lt;/li&gt;
&lt;li&gt;Move the noisy database to its own instance if needed&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="apps-cannot-connect-after-migration"&gt;Apps Cannot Connect After Migration
&lt;/h3&gt;&lt;p&gt;I know what you&amp;rsquo;re thinking: &amp;ldquo;I updated the connection string, why isn&amp;rsquo;t it working?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Checklist:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Verify &lt;code&gt;pg_hba.conf&lt;/code&gt; allows the Docker subnet&lt;/li&gt;
&lt;li&gt;Check firewall rules&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;pg_isready&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one&amp;rsquo;s saved me more times than I can count. If &lt;code&gt;pg_isready&lt;/code&gt; fails, your app&amp;rsquo;s not going to connect either.&lt;/p&gt;
&lt;h3 id="backups-take-too-long"&gt;Backups Take Too Long
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Options:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Switch from logical to physical backups&lt;/li&gt;
&lt;li&gt;Run dumps in parallel&lt;/li&gt;
&lt;li&gt;Exclude rarely changed databases from daily dumps&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="version-conflicts"&gt;Version Conflicts
&lt;/h3&gt;&lt;p&gt;You can&amp;rsquo;t mix PostgreSQL major versions in one instance. Period.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upgrade all databases together&lt;/li&gt;
&lt;li&gt;Or run a second instance temporarily during migration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yeah, this part&amp;rsquo;s finicky. Plan your upgrades carefully.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I prevent one database from starving others?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use connection limits, PgBouncer, and per-database settings. Monitor resource usage and be ready to split workloads if necessary.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the minimum VM size for 5 to 10 databases?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For typical homelab apps, 4 cores and 16GB RAM is a solid baseline. For smaller setups, 2 cores and 8GB works fine. Scale up if you see sustained load.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I mix PostgreSQL versions in one instance?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. One instance equals one major version. Plan upgrades accordingly.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How does backup time change with consolidation?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Backups are usually faster and simpler because you avoid container overhead and duplicated jobs. One script instead of six.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is a VM really better than LXC?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For PostgreSQL, yes in most cases. VMs provide better I/O isolation and simpler snapshot workflows.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What apps work best with a shared Postgres VM?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Apps with moderate workloads and good external database support, like Home Assistant, Wiki.js, and monitoring tools.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I handle high-write workloads?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Tune WAL settings, use fast storage, and consider isolating that workload if it dominates the system.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does consolidation reduce Docker overhead?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. You eliminate multiple Postgres images, volumes, and background processes.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How can I migrate with minimal downtime?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Dump and restore during low-usage windows, or stage databases one at a time. For a homelab, a few minutes of downtime&amp;rsquo;s usually fine.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What tools help monitor contention?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Prometheus with a Postgres exporter, pg_stat_statements, and log analyzers are all effective.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Are there security risks?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The risk shifts from container boundaries to role and permission management. Follow least-privilege principles and you&amp;rsquo;ll be fine.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/postgresql_hu_9f46bcd831d480b3.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/postgresql_hu_43ed92609cc4bb42.webp" width="100" height="100" alt="PostgreSQL Official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://www.postgresql.org/docs/current/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Consolidating all or some of your PostgreSQL databases into one dedicated Postgres VM is one of those changes that feels scary at first and obvious in hindsight. You trade a bit of isolation for dramatically simpler management, cleaner backups, and better resource utilization.&lt;/p&gt;
&lt;p&gt;For my homelab, moving away from six separate containers was a relief. One upgrade, one backup strategy, one place to look when something goes wrong.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re juggling multiple Postgres containers today, consider consolidation. Start small, monitor closely, and split workloads only when the data tells you to. Your future self, restoring a database in the middle of the night, will thank you.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5)&lt;/strong&gt;
&lt;strong&gt;Nice to have but not required.&lt;/strong&gt;
Why it fits this post: This mini PC offers a balance of performance, quiet operation, and expandability for a PostgreSQL VM, but may be more limited for heavy multi-database or high-I/O scenarios compared to workstation-class options.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Kodi vs Jellyfin (2026): Honest Comparison + Which to Pick</title><link>https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/</link><pubDate>Sat, 10 Jan 2026 07:12:29 -0700</pubDate><guid>https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/featured_hu_59d12c317f1f53c8.webp" alt="Featured image of post Kodi vs Jellyfin (2026): Honest Comparison + Which to Pick" /&gt;&lt;p&gt;Jellyfin vs Kodi: which media server is better for you? If you&amp;rsquo;ve ever said &amp;ldquo;Kodi is awesome, but&amp;hellip;&amp;rdquo; and then sighed, this post is for you.&lt;/p&gt;
&lt;p&gt;Kodi was my media center for years. I loved the skins, the endless tweaks, and the feeling that I could make it do anything. But once it had to work for my wife, friends, and multiple TVs, Kodi stopped being a fun hobby and turned into an ongoing tech support job.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when I finally ditched Kodi as my primary media player and moved to Jellyfin. Not Kodi with network shares. Not Kodi with a bunch of sync hacks. Not even Kodi with the Jellyfin plugin, which is good but still not the clean fix I wanted.&lt;/p&gt;
&lt;p&gt;I switched to Jellyfin server and Jellyfin clients. It solved the three things that made Kodi painful: syncing watched status across devices, transcoding files that wouldn&amp;rsquo;t play, and sharing media remotely without being a tech support desk.&lt;/p&gt;
&lt;p&gt;This article explains why, from the perspective of someone who&amp;rsquo;s run both for years.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Kodi is a powerful front-end player, but Jellyfin is a true media server with simple clients. Jellyfin wins for transcoding, remote access, sharing with friends, and keeping watched status synced everywhere. For most households, that makes it the better choice.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="kodi-vs-jellyfin-the-mental-model-that-changes-everything"&gt;Kodi vs Jellyfin: The Mental Model That Changes Everything
&lt;/h2&gt;&lt;p&gt;Before we get into features, you need to understand one core difference. This alone explains why Jellyfin feels easier once you switch.&lt;/p&gt;
&lt;h3 id="kodi-a-player-first"&gt;Kodi: A Player First
&lt;/h3&gt;&lt;p&gt;Kodi is designed to run on the device connected to your TV. Each Kodi box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scans media itself&lt;/li&gt;
&lt;li&gt;Maintains its own library database&lt;/li&gt;
&lt;li&gt;Tracks watched status locally&lt;/li&gt;
&lt;li&gt;Depends on the device&amp;rsquo;s hardware to play files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You point Kodi at local files or network shares, and it works great for that use case. But every device is effectively on its own unless you bolt on extra layers like a shared database or third-party services (Trakt, for example).&lt;/p&gt;
&lt;p&gt;And look, setting up MySQL database sharing across Kodi instances? That&amp;rsquo;s a weekend project that&amp;rsquo;ll break in a few months. I could never keep my shared library alive longer than that.&lt;/p&gt;
&lt;h3 id="jellyfin-a-server-with-clients"&gt;Jellyfin: A Server With Clients
&lt;/h3&gt;&lt;p&gt;Jellyfin flips the model.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One Jellyfin server indexes your entire library&lt;/li&gt;
&lt;li&gt;All metadata, artwork, and watched status live in one place&lt;/li&gt;
&lt;li&gt;Every device connects as a client&lt;/li&gt;
&lt;li&gt;The server decides whether to direct-play or transcode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most people, one central server is simpler than maintaining multiple Kodi installations. You&amp;rsquo;re managing one thing instead of five.&lt;/p&gt;
&lt;h2 id="getting-started-what-jellyfin-setup-looks-like"&gt;Getting Started: What Jellyfin Setup Looks Like
&lt;/h2&gt;&lt;p&gt;You don&amp;rsquo;t need an enterprise server to run Jellyfin. Honestly, you probably have something lying around that&amp;rsquo;ll work.&lt;/p&gt;
&lt;h3 id="where-jellyfin-can-run"&gt;Where Jellyfin Can Run
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;NAS systems like Unraid or TrueNAS&lt;/li&gt;
&lt;li&gt;Mini PCs and Intel NUCs&lt;/li&gt;
&lt;li&gt;Old desktops&lt;/li&gt;
&lt;li&gt;Even Raspberry Pi for light use (though transcoding will not work well)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="beginner-setup-reality-check"&gt;Beginner Setup Reality Check
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Install Jellyfin server&lt;/strong&gt; - Runs on almost any computer; it doesn&amp;rsquo;t need much
&lt;ul&gt;
&lt;li&gt;Intel 8th Gen or newer if you want hardware transcoding&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Point it at your media folders&lt;/strong&gt; - Movies, TV Shows, Music
&lt;ul&gt;
&lt;li&gt;Can be network storage like Unraid, OpenMediaVault, or a plain NAS share&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let it scan and download metadata&lt;/strong&gt; - This takes a while the first time&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install the Jellyfin app&lt;/strong&gt; on your streaming device
&lt;ul&gt;
&lt;li&gt;NVIDIA Shield, Smart TV, or even some gaming consoles&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log in and start watching&lt;/strong&gt; - That&amp;rsquo;s it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The whole process takes maybe an hour if you already have media organized. Compare that to setting up Kodi on every TV in the house, then explaining to your spouse why the living room shows different watch progress from the bedroom.&lt;/p&gt;
&lt;h3 id="hardware-expectations"&gt;Hardware Expectations
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s the thing: hardware matters, but not as much as you&amp;rsquo;d think.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mostly direct play, one or two users&lt;/strong&gt;: any old desktop works fine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple remote users, lots of transcoding&lt;/strong&gt;: Intel Quick Sync (Intel 8th Gen or newer) or a GPU helps massively&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak CPU with no hardware acceleration&lt;/strong&gt;: expect stuttering when transcoding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re only watching locally and your files are already in formats your devices support, you can run Jellyfin on a potato. It&amp;rsquo;s when you start transcoding 4K movies for your friend&amp;rsquo;s phone that you need real hardware.&lt;/p&gt;
&lt;h2 id="why-jellyfin-beat-kodi-in-my-house"&gt;Why Jellyfin Beat Kodi in My House
&lt;/h2&gt;&lt;p&gt;Kodi was fine when it was only me. I could tinker, fix things, restart services. But once my wife started using it daily, the friction became obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different Kodi boxes had different watched statuses&lt;/li&gt;
&lt;li&gt;One update broke a skin and suddenly nothing looked familiar&lt;/li&gt;
&lt;li&gt;Some files played on one TV but not another&lt;/li&gt;
&lt;li&gt;Explaining &amp;ldquo;back out and refresh the library&amp;rdquo; got old fast&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried Kodi with the Jellyfin plugin, and while it helped with library sync, it still left me maintaining two layers: Jellyfin plus Kodi on every device. Every Kodi update was a potential disaster. Every new device meant configuring Kodi again.&lt;/p&gt;
&lt;p&gt;Switching to native Jellyfin clients finally solved the problem. One app, consistent interface, no per-device configuration hell.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B07YP9FBMM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_84a3675c3ec53e96.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_541c32e7cf211fab.webp" width="600" height="284" alt="NVIDIA SHIELD Pro" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;NVIDIA SHIELD Pro&lt;/strong&gt;&lt;br&gt;
A premium Android TV client, it offers excellent Jellyfin playback, smooth 4K HDR support, and broad codec compatibility, making it a top choice for users wanting a seamless living room experience with Jellyfin.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4gZhtU2" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="jellyfin-transcoding-making-files-play-everywhere"&gt;Jellyfin Transcoding: Making Files Play Everywhere
&lt;/h2&gt;&lt;p&gt;Alright, this is where Jellyfin pulls ahead of Kodi for good.&lt;/p&gt;
&lt;p&gt;Kodi plays files locally. If the device can&amp;rsquo;t decode the file, playback fails or stutters. Kodi has no server-side transcoding. Everything happens on the client.&lt;/p&gt;
&lt;p&gt;Jellyfin includes a full transcoding engine based on FFmpeg. This means the server can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Convert unsupported codecs on the fly&lt;/li&gt;
&lt;li&gt;Downscale 4K video to 1080p for older TVs&lt;/li&gt;
&lt;li&gt;Adjust bitrate for slow connections&lt;/li&gt;
&lt;li&gt;Convert audio formats like TrueHD or DTS-HD to stereo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Translation: your phone can now play that 4K HDR movie because Jellyfin converts it to something your phone understands, in real time.&lt;/p&gt;
&lt;p&gt;Your friend with the ancient Fire Stick? They can watch your 4K Blu-ray rips because Jellyfin handles the conversion. With Kodi, they&amp;rsquo;d get a black screen or a stuttering mess.&lt;/p&gt;
&lt;h3 id="hardware-acceleration-warning"&gt;Hardware Acceleration Warning
&lt;/h3&gt;&lt;p&gt;Jellyfin supports hardware-accelerated transcoding on Intel Quick Sync, NVIDIA NVENC, and AMD VA-API. When it works, your server can handle multiple streams without maxing out the CPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When it doesn&amp;rsquo;t work&lt;/strong&gt;: You get stuttering, buffering, or the server crashes under load.&lt;/p&gt;
&lt;p&gt;I know what you&amp;rsquo;re thinking: &amp;ldquo;how hard can it be?&amp;rdquo; Well, driver issues on Linux are real. Wrong permissions for hardware devices will bite you. Codec support varies by GPU generation. Test hardware transcoding with one stream before depending on it, because finding out it doesn&amp;rsquo;t work when three people are trying to watch at once is not fun.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/featured_hu_bcbeda2d4c823bcf.avif"&gt;&lt;img src="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/featured_hu_ed313df434e7c8bd.webp" width="100" height="100" alt="Jellyfin &amp;#43; Intel QuickSync in Unprivileged LXC - The Complete Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Jellyfin + Intel QuickSync - The Complete Guide&lt;/strong&gt;&lt;br&gt;
How to install and configure Jellyfin in an unprivileged LXC container with QuickSync.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
View Article
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="centralized-library-and-watched-status-sync"&gt;Centralized Library and Watched Status Sync
&lt;/h2&gt;&lt;p&gt;Jellyfin tracks watched episodes, movies, and resume positions down to the second. Start a show on your living room TV, pause halfway through, and resume on your phone in bed. It works.&lt;/p&gt;
&lt;p&gt;Kodi can sync watched status, but only if you set up a shared MySQL database, use third-party services like Trakt, or run Kodi as a front-end to Jellyfin. Jellyfin does it by default. No configuration, no third-party accounts, no database setup.&lt;/p&gt;
&lt;p&gt;This centralized approach extends to all metadata, artwork, and library organization. Change something once on the server, and every client sees the update immediately.&lt;/p&gt;
&lt;h2 id="remote-access-sharing-media-without-pain"&gt;Remote Access: Sharing Media Without Pain
&lt;/h2&gt;&lt;p&gt;This is where Jellyfin completely outclasses Kodi.&lt;/p&gt;
&lt;h3 id="kodi-remote-sharing-reality"&gt;Kodi Remote Sharing Reality
&lt;/h3&gt;&lt;p&gt;Yes, you can share Kodi libraries remotely using VPNs or SMB shares. But it usually involves explaining network paths, teaching friends how to mount drives, or giving out file share credentials.&lt;/p&gt;
&lt;p&gt;And then they call you because it stopped working after a Windows update. Or their router rebooted. Or they got a new phone and don&amp;rsquo;t remember the setup steps.&lt;/p&gt;
&lt;h3 id="jellyfin-is-designed-for-this"&gt;Jellyfin Is Designed for This
&lt;/h3&gt;&lt;p&gt;Jellyfin includes user accounts, per-library permissions, bandwidth limits, and secure authenticated access. You can expose Jellyfin using port forwarding with HTTPS, a reverse proxy, or a mesh VPN like Tailscale.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
If you port forward Jellyfin, use HTTPS and strong passwords. Web-facing media servers are targets. Don&amp;rsquo;t be the person who gets their server compromised because they used &amp;ldquo;password123.&amp;rdquo;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Friends install an app, log in, and watch. Because of transcoding, it works even on weak devices. No explaining network shares. No VPN configuration. No tech support calls.&lt;/p&gt;
&lt;h2 id="common-jellyfin-pain-points-to-expect"&gt;Common Jellyfin Pain Points to Expect
&lt;/h2&gt;&lt;p&gt;Look, Jellyfin isn&amp;rsquo;t perfect. Here&amp;rsquo;s what&amp;rsquo;ll probably trip you up:&lt;/p&gt;
&lt;h3 id="hardware-transcoding-failures"&gt;Hardware Transcoding Failures
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Driver issues on Linux (especially NVIDIA)&lt;/li&gt;
&lt;li&gt;Wrong permissions for hardware devices (&lt;code&gt;/dev/dri&lt;/code&gt; access problems)&lt;/li&gt;
&lt;li&gt;Codec support varies by GPU generation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When hardware transcoding fails, Jellyfin falls back to software transcoding, which will max out your CPU. You&amp;rsquo;ll know because your server fans will sound like a jet engine.&lt;/p&gt;
&lt;h3 id="weak-server-performance"&gt;Weak Server Performance
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Software transcoding taxes weak CPUs&lt;/li&gt;
&lt;li&gt;Multiple 4K transcodes need serious hardware&lt;/li&gt;
&lt;li&gt;Remote users expect things to work on the first try regardless of your setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re running on an old laptop with a dual-core CPU, don&amp;rsquo;t expect to transcode 4K to three people simultaneously. It won&amp;rsquo;t happen.&lt;/p&gt;
&lt;h3 id="android-tv-app-limitations"&gt;Android TV App Limitations
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Some cheap Android TV boxes struggle with the official app&lt;/li&gt;
&lt;li&gt;HDR passthrough can be finicky&lt;/li&gt;
&lt;li&gt;Third-party clients like Findroid sometimes work better&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The official Jellyfin Android TV app is solid, but if you&amp;rsquo;ve got a $30 Android box from Amazon, you might have issues. Try Findroid if the official app gives you trouble.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;br&gt;
Large, affordable storage is essential for a growing media library, and this drive provides ample capacity for movies and shows, though it lacks NAS/enterprise features for heavy multi-user or RAID use.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-native-jellyfin-beats-kodi-plus-the-jellyfin-plugin"&gt;Why Native Jellyfin Beats Kodi Plus the Jellyfin Plugin
&lt;/h2&gt;&lt;p&gt;The Jellyfin plugin for Kodi is excellent. It turns Kodi into a Jellyfin client, syncing libraries and watched status. I used it for months.&lt;/p&gt;
&lt;p&gt;But with Kodi plus Jellyfin, you still manage Kodi settings on every device. A Kodi or Jellyfin update can break the plugin. Native Jellyfin clients remove an entire layer of troubleshooting.&lt;/p&gt;
&lt;p&gt;My rule: I use Kodi plus Jellyfin for myself when I want customization. I never give Kodi to friends or family. They get native Jellyfin clients, because I value my free time.&lt;/p&gt;
&lt;h2 id="troubleshooting-common-issues"&gt;Troubleshooting Common Issues
&lt;/h2&gt;&lt;h3 id="playback-stutters-or-buffers"&gt;Playback Stutters or Buffers
&lt;/h3&gt;&lt;p&gt;First, check if the stream is transcoding or direct playing. In the Jellyfin dashboard, you can see active streams and whether they&amp;rsquo;re transcoding.&lt;/p&gt;
&lt;p&gt;If it&amp;rsquo;s transcoding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable hardware acceleration if you haven&amp;rsquo;t already&lt;/li&gt;
&lt;li&gt;Lower client bitrate for weak devices&lt;/li&gt;
&lt;li&gt;Consider pre-converting very high bitrate files if you&amp;rsquo;re hitting this constantly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it&amp;rsquo;s direct playing and still stuttering, your network&amp;rsquo;s probably the issue.&lt;/p&gt;
&lt;h3 id="high-cpu-usage-on-the-server"&gt;High CPU Usage on the Server
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Too many software transcodes happening at once&lt;/li&gt;
&lt;li&gt;Enable hardware transcoding if available&lt;/li&gt;
&lt;li&gt;Consider pre-converting very high bitrate files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you see 100% CPU usage and your server&amp;rsquo;s crawling, someone&amp;rsquo;s transcoding without hardware acceleration. Fix that first.&lt;/p&gt;
&lt;h3 id="remote-access-works-locally-but-not-outside"&gt;Remote Access Works Locally but Not Outside
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Verify port forwarding or VPN configuration&lt;/li&gt;
&lt;li&gt;Confirm users are connecting to the correct external address&lt;/li&gt;
&lt;li&gt;Check firewall rules on both the server and router&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few minutes testing this yourself saves an hour or more of back-and-forth with friends who can&amp;rsquo;t connect.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is Jellyfin completely free like Kodi?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Jellyfin is fully open source and free, with no paid tiers. No premium features locked behind paywalls, no subscriptions, nothing.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need a powerful server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not for basic use. You only need more power if you expect multiple transcodes at once. If you&amp;rsquo;re mostly direct playing to local devices, an old desktop is fine.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I still use Kodi with Jellyfin?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Absolutely. Kodi plus the Jellyfin plugin is a great power-user setup. I still use it on my main TV because I like the customization. But I don&amp;rsquo;t inflict it on anyone else.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is Jellyfin as customizable as Kodi?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No, and that&amp;rsquo;s intentional. Jellyfin favors simplicity over endless customization. You can theme it a bit, but you&amp;rsquo;re not building custom skins or installing dozens of add-ons.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will Jellyfin always transcode my files?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Jellyfin direct-plays whenever possible and only transcodes when needed. If your client supports the file format, codec, and resolution, Jellyfin streams it directly. Transcoding only kicks in when something doesn&amp;rsquo;t match.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-why-jellyfin-won-for-me"&gt;Conclusion: Why Jellyfin Won for Me
&lt;/h2&gt;&lt;p&gt;Kodi is still amazing software. If you love tweaking and customizing, Kodi will always have a place. I&amp;rsquo;m not saying Kodi is bad, I&amp;rsquo;m saying it&amp;rsquo;s designed for a different use case.&lt;/p&gt;
&lt;p&gt;But for most people, especially families and shared households, Jellyfin is the better tool. One server, synced playback everywhere, reliable transcoding, easy remote access, and simple apps that work.&lt;/p&gt;
&lt;p&gt;I ditched Kodi not because it failed, but because Jellyfin solved the problems Kodi was never designed to solve. If you&amp;rsquo;re tired of being the household media IT department, Jellyfin might be your way out.&lt;/p&gt;
&lt;p&gt;This post is a straight Kodi-vs-Jellyfin call. If Plex and Emby are also on your shortlist, the four-way showdown weighs all of them on features, cost, hardware, and ease of use:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/bestsoftware_main2_hu_9d8e934a4d13a93c.avif"&gt;&lt;img src="https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/bestsoftware_main2_hu_9d2677f1ba246a6d.webp" width="100" height="100" alt="Kodi vs Plex vs Jellyfin vs Emby: Which Is Best in 2026?" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Kodi vs Plex vs Jellyfin vs Emby: Which Is Best in 2026?&lt;/strong&gt;
Weighing Plex or Emby too? The four-way showdown compares all of them on features, cost, and ease of use.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read the Showdown
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Ready to set up your own Jellyfin server? Check out the &lt;a class="link" href="https://jellyfin.org/docs/" target="_blank" rel="noopener"
&gt;official Jellyfin documentation&lt;/a&gt; for installation guides and best practices.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;&lt;br&gt;
This dedicated GPU enables efficient hardware transcoding for Jellyfin, especially useful if you have multiple users or diverse client devices, but it&amp;rsquo;s not required if your clients can direct play all your content.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://jellyfin.org/images/logo.svg" alt="Jellyfin Official Documentation" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Jellyfin Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://jellyfin.org/docs/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://forum.kodi.tv/kodi/theme/images/logo.png" alt="Kodi Wiki" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Kodi Wiki&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://kodi.wiki/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://www.intel.com/content/dam/logos/intel-header-logo.svg" alt="Intel Quick Sync Video Guide" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Intel Quick Sync Video Guide&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://www.intel.com/content/www/us/en/architecture-and-technology/quick-sync-video/quick-sync-video-general.html"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How to Install and Configure Fail2Ban on your Jellyfin LXC</title><link>https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/</link><pubDate>Thu, 01 Jan 2026 07:03:16 -0700</pubDate><guid>https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/</guid><description>&lt;img src="https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/featured_hu_4ebd4a6930ec1937.webp" alt="Featured image of post How to Install and Configure Fail2Ban on your Jellyfin LXC" /&gt;&lt;p&gt;If you&amp;rsquo;ve exposed Jellyfin to the internet, whether through port forwarding or a reverse proxy, you&amp;rsquo;re already being scanned. Bots see an open login page and start stuffing passwords, trying to guess their way in.&lt;/p&gt;
&lt;p&gt;Ask me how I know.&lt;/p&gt;
&lt;p&gt;When I first stood up Jellyfin and exposed it to the internet, I left the default &lt;code&gt;jellyfin&lt;/code&gt; username in place. Bots found the login page almost immediately and hammered the account around the clock. I only noticed because Jellyfin&amp;rsquo;s per-user lockout kept tripping and I couldn&amp;rsquo;t log in to watch anything. I knew better. And there I was, staring at thousands of failed login attempts from IPs scattered across multiple countries.&lt;/p&gt;
&lt;p&gt;So I changed the default username. That stopped the account lockouts. But the server itself was still getting probed every single day. I needed another layer. Fail2Ban was the obvious answer.&lt;/p&gt;
&lt;p&gt;This guide covers installing and configuring Fail2Ban for a Jellyfin LXC without banning yourself. We&amp;rsquo;ll cover direct exposure, reverse proxy setups, Docker-in-LXC gotchas, and monitoring bans over time.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
&lt;p&gt;Fail2Ban watches Jellyfin logs for failed login attempts and automatically blocks abusive IPs. With the right jail, filter, and network configuration, you can stop brute-force attacks in their tracks even behind a reverse proxy all without locking yourself out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick Start for Experienced Users&lt;/strong&gt;:&lt;br&gt;
Create filter &lt;code&gt;/etc/fail2ban/filter.d/jellyfin.conf&lt;/code&gt;&lt;br&gt;
Create jail &lt;code&gt;/etc/fail2ban/jail.d/jellyfin.local&lt;/code&gt;&lt;br&gt;
Verify firewall backend (iptables or nftables)&lt;br&gt;
Configure reverse proxy trust if needed&lt;br&gt;
Test with &lt;code&gt;fail2ban-regex&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-fail2ban-protects-your-jellyfin-server"&gt;Why Fail2Ban Protects Your Jellyfin Server
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing: Jellyfin has authentication, and its rate-limit lockout is per user, not global. If you haven&amp;rsquo;t set it on a user account, an attacker can throw thousands of passwords at it per hour without consequence.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/jellyfin-lockout_hu_18803420e51bbe72.avif 800w, https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/jellyfin-lockout_hu_8c76558aa92844bb.avif 1030w"&gt;&lt;img src="https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/jellyfin-lockout_hu_9105dfb1c19cb306.webp" srcset="https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/jellyfin-lockout_hu_9105dfb1c19cb306.webp 800w, https://diymediaserver.com/post/2026/install-configure-fail2ban-jellyfin-lxc/jellyfin-lockout_hu_56d4534b3bd37c58.webp 1030w" width="800" height="145" alt="Jellyfin&amp;#39;s Lockout setting" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;Fail2Ban operates globally. You don&amp;rsquo;t have to set it per user.&lt;/p&gt;
&lt;p&gt;Fail2Ban does this by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Watching log files for suspicious patterns&lt;/li&gt;
&lt;li&gt;Counting repeated failures from the same IP&lt;/li&gt;
&lt;li&gt;Adding firewall rules to block that IP for a set time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For context, the Jellyfin default HTTP port is 8096 and the default HTTPS port is 8920. Whether you expose these directly or run Jellyfin behind a reverse proxy on port 443, failed login attempts look the same in the logs. That&amp;rsquo;s actually good news. One Fail2Ban config works for both scenarios.&lt;/p&gt;
&lt;h2 id="pre-deployment-checklist"&gt;Pre-Deployment Checklist
&lt;/h2&gt;&lt;p&gt;Answer these before touching any configs. Trust me, five minutes now saves an hour of troubleshooting later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Exposure method&lt;/strong&gt;: Direct port forwarding or reverse proxy?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Jellyfin port&lt;/strong&gt;: Using default 8096 or custom?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Firewall backend&lt;/strong&gt;: iptables or nftables? (Check with &lt;code&gt;sudo iptables --version&lt;/code&gt; and &lt;code&gt;sudo nft --version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Container setup&lt;/strong&gt;: Jellyfin directly in LXC or Docker-in-LXC?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Reverse proxy&lt;/strong&gt;: If yes, are real client IPs logged?&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Trusted networks&lt;/strong&gt;: What IPs should never be banned?&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
&lt;p&gt;I&amp;rsquo;ll be configuring this on an unprivileged LXC running Debian 13 with nftables.&lt;/p&gt;
&lt;p&gt;All examples below assume that setup.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="checking-your-firewall-backend"&gt;Checking Your Firewall Backend
&lt;/h3&gt;&lt;p&gt;Modern Debian systems often run nftables with iptables as a compatibility layer.&lt;br&gt;
You need to know which one Fail2Ban will use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check what&amp;#39;s actually running&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status nftables
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status netfilter-persistent
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fail2Ban works with both, but the actions you configure will differ.&lt;br&gt;
Get this wrong and bans won&amp;rsquo;t happen.&lt;/p&gt;
&lt;h2 id="installing-fail2ban-on-debianubuntu-lxc"&gt;Installing Fail2Ban on Debian/Ubuntu LXC
&lt;/h2&gt;&lt;p&gt;Alright, let&amp;rsquo;s get Fail2Ban installed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install fail2ban
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status fail2ban
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see &lt;code&gt;active (running)&lt;/code&gt;, you&amp;rsquo;re golden. Fail2Ban starts automatically and survives reboots.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;● fail2ban.service - Fail2Ban Service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/usr/lib/systemd/system/fail2ban.service&lt;span class="p"&gt;;&lt;/span&gt; enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active: active &lt;span class="o"&gt;(&lt;/span&gt;running&lt;span class="o"&gt;)&lt;/span&gt; since Wed 2025-12-31 11:22:01 MST&lt;span class="p"&gt;;&lt;/span&gt; 20h ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Invocation: 9a7ce4195aaa4dc48dc750363bb9b954
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Docs: man:fail2ban&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Main PID: &lt;span class="m"&gt;329267&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;fail2ban-server&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Tasks: &lt;span class="m"&gt;13&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;limit: 7051&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Memory: 15.7M &lt;span class="o"&gt;(&lt;/span&gt;peak: 24M&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CPU: 4min 37.380s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CGroup: /system.slice/fail2ban.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─329267 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Dec &lt;span class="m"&gt;31&lt;/span&gt; 11:22:01 racknerd-ea37d8f systemd&lt;span class="o"&gt;[&lt;/span&gt;1&lt;span class="o"&gt;]&lt;/span&gt;: Started fail2ban.service - Fail2Ban Service.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Dec &lt;span class="m"&gt;31&lt;/span&gt; 11:22:01 racknerd-ea37d8f fail2ban-server&lt;span class="o"&gt;[&lt;/span&gt;329267&lt;span class="o"&gt;]&lt;/span&gt;: Server ready
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;
This mini workstation is ideal for running Jellyfin in an LXC container and experimenting with Fail2Ban, offering enough power and networking for a secure, flexible homelab setup.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The hardware doesn&amp;rsquo;t really matter for Fail2Ban itself. It runs happily on anything that can host the LXC. What matters next is the filter that tells it what a failed login looks like.&lt;/p&gt;
&lt;h2 id="creating-the-jellyfin-fail2ban-filter"&gt;Creating the Jellyfin Fail2Ban Filter
&lt;/h2&gt;&lt;p&gt;Filters live in &lt;code&gt;/etc/fail2ban/filter.d/&lt;/code&gt; and tell Fail2Ban what a &amp;ldquo;failed login&amp;rdquo; looks like in your logs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/fail2ban/filter.d/jellyfin.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Definition]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;failregex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;^.*Authentication request for .* has been denied \(IP: &amp;#34;&amp;lt;ADDR&amp;gt;&amp;#34;\)\.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ignoreregex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Jellyfin log formats can change between versions. I learned this the hard way after an update when bans mysteriously stopped working. Always test your filter after upgrades.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="test-the-filter"&gt;Test the Filter
&lt;/h3&gt;&lt;p&gt;Before going any further:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Make sure to have some failed logins today&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Test the filter against your actual logs
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; REPLACE: YYYYMMDD with today&amp;rsquo;s date&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fail2ban-regex /var/log/jellyfin/jellyfinYYYYMMDD.log /etc/fail2ban/filter.d/jellyfin.conf --print-all-matched
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see matched lines with IP addresses highlighted.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;2025-12-31 07:22:27.102 -07:00&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;INF&lt;span class="o"&gt;]&lt;/span&gt; Authentication request &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test@test&amp;#34;&lt;/span&gt; has been denied &lt;span class="o"&gt;(&lt;/span&gt;IP: &lt;span class="s2"&gt;&amp;#34;140.32.72.74&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;2025-12-31 07:22:28.787 -07:00&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;INF&lt;span class="o"&gt;]&lt;/span&gt; Authentication request &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test@test&amp;#34;&lt;/span&gt; has been denied &lt;span class="o"&gt;(&lt;/span&gt;IP: &lt;span class="s2"&gt;&amp;#34;140.32.72.74&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;2025-12-31 07:41:08.539 -07:00&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;INF&lt;span class="o"&gt;]&lt;/span&gt; Authentication request &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test@test&amp;#34;&lt;/span&gt; has been denied &lt;span class="o"&gt;(&lt;/span&gt;IP: &lt;span class="s2"&gt;&amp;#34;140.32.72.74&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;2025-12-31 07:41:10.368 -07:00&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;INF&lt;span class="o"&gt;]&lt;/span&gt; Authentication request &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test@test&amp;#34;&lt;/span&gt; has been denied &lt;span class="o"&gt;(&lt;/span&gt;IP: &lt;span class="s2"&gt;&amp;#34;140.32.72.74&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Zero matches means your regex is wrong or your log path is incorrect.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Don&amp;rsquo;t skip this step. I&amp;rsquo;ve wasted hours debugging jails that would never work because the filters never matched anything.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="creating-the-jellyfin-jail-configuration"&gt;Creating the Jellyfin Jail Configuration
&lt;/h2&gt;&lt;p&gt;Jails define what happens when the filter finds matches.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/fail2ban/jail.d/jellyfin.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="base-configuration-without-a-reverse-proxy"&gt;Base Configuration (Without a Reverse Proxy)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[jellyfin]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;jellyfin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;logpath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/jellyfin/jellyfin*.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;polling&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;maxretry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;findtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;bantime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8096,8920&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nftables[type=multiport]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ignoreip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1/8 ::1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Optional: add YOUR LAN subnet to ignoreip (recommended)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Examples (pick the one that matches your network, or use the exact /24 you use)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 10.0.0.0/8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 172.16.0.0/12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 192.168.0.0/16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That config means: 5 failures within 10 minutes earns a 20-minute ban. Tune to taste. Don&amp;rsquo;t make &lt;code&gt;maxretry&lt;/code&gt; too low or &lt;code&gt;bantime&lt;/code&gt; too long. You&amp;rsquo;ll lock yourself out the first time you mistype a password while trying to watch something away from home.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;code&gt;backend = polling&lt;/code&gt; is needed because Jellyfin creates a new log file every 24 hours with a unique name. This lets fail2ban read these files.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That&amp;rsquo;s the basics of Fail2Ban when you&amp;rsquo;re exposing Jellyfin directly to the internet via port forwarding. I strongly recommend a reverse proxy rather than exposing Jellyfin directly.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll cover the reverse-proxy setup in a follow-up post.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
The Raspberry Pi is a budget-friendly, low-power option if you are wanting to test Fail2Ban and Jellyfin in a lightweight, isolated environment before deploying to larger servers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A spare Pi is a great place to break things before you touch the LXC running your real library. Once the jail behaves there, copy the configs over with confidence.&lt;/p&gt;
&lt;h2 id="monitoring-and-maintenance"&gt;Monitoring and Maintenance
&lt;/h2&gt;&lt;h3 id="watch-ban-activity"&gt;Watch Ban Activity
&lt;/h3&gt;&lt;p&gt;Want to see bans happen in real-time? This is oddly satisfying:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tail -f /var/log/fail2ban.log
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see IPs getting banned as they trip your thresholds. After the first few, you realize how many bots are constantly probing your server.&lt;/p&gt;
&lt;h3 id="unban-yourself"&gt;Unban Yourself
&lt;/h3&gt;&lt;p&gt;Locked yourself out? It happens. From the LXC console:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fail2ban-client &lt;span class="nb"&gt;set&lt;/span&gt; jellyfin unbanip 192.168.1.50
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace with your actual IP. You&amp;rsquo;ll be unbanned immediately.&lt;/p&gt;
&lt;h3 id="best-practices-for-fail2ban-management"&gt;Best Practices for Fail2Ban Management
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;bantime&lt;/code&gt; finite. Infinite bans sound appealing, but they&amp;rsquo;re risky. You can ban yourself permanently.&lt;/li&gt;
&lt;li&gt;Test filters after every Jellyfin update. Log formats change.&lt;/li&gt;
&lt;li&gt;Review ban logs monthly for patterns. If you&amp;rsquo;re getting hammered from specific countries, consider additional firewall rules.&lt;/li&gt;
&lt;li&gt;Document your unban procedure somewhere you can access when locked out.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="troubleshooting-common-fail2ban-issues"&gt;Troubleshooting Common Fail2Ban Issues
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;No bans occur&lt;/strong&gt;: Wrong filter regex, wrong logpath, or Jellyfin&amp;rsquo;s logging level is too low. Use &lt;code&gt;fail2ban-regex&lt;/code&gt; to debug. Also check that Fail2Ban is reading the log file. Permissions matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Locked out completely&lt;/strong&gt;: Access your LXC console (not SSH, that&amp;rsquo;s blocked too), unban manually, then add your IP to &lt;code&gt;ignoreip&lt;/code&gt; in the jail config.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will Fail2Ban ban my own IP?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, if you exceed &lt;code&gt;maxretry&lt;/code&gt;. Use &lt;code&gt;ignoreip&lt;/code&gt; for your home network or keep the unban command handy. I&amp;rsquo;ve locked myself out more times than I&amp;rsquo;d like to admit.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the difference between bantime and findtime?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;code&gt;findtime&lt;/code&gt; is the detection window, how far back Fail2Ban looks for failures. &lt;code&gt;bantime&lt;/code&gt; is how long the ban lasts. So with &lt;code&gt;findtime = 600&lt;/code&gt; and &lt;code&gt;maxretry = 3&lt;/code&gt;, you get banned if you fail 3 times within 10 minutes. The ban then lasts for &lt;code&gt;bantime&lt;/code&gt; seconds.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does Fail2Ban work with nftables?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, but verify the backend configuration matches your system. Modern Debian uses nftables, but Fail2Ban might still default to iptables compatibility mode. Check your jail config.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is Fail2Ban useful for local-only Jellyfin?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not really. It matters when Jellyfin is exposed beyond your LAN.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-harden-your-jellyfin-installation"&gt;Conclusion: Harden Your Jellyfin Installation
&lt;/h2&gt;&lt;p&gt;If your Jellyfin instance is reachable from the internet, Fail2Ban is essential. Brute-force attempts are constant and invisible until you check your logs. I was shocked when I first looked. Thousands of attempts per day from IPs all over the world.&lt;/p&gt;
&lt;p&gt;Understand your network topology, configure the correct jail and action, and monitor bans periodically. That&amp;rsquo;s how you turn Jellyfin from a soft target into a more secure service. This is the third step in hardening your Jellyfin LXC.&lt;/p&gt;
&lt;p&gt;You should have already:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Be using an unprivileged LXC - &lt;a class="link" href="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/" target="_blank" rel="noopener"
&gt;LXC Guide for Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Set Firewall Rules&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Secured your SSH - &lt;a class="link" href="https://diymediaserver.com/post/complete-guide-securing-ssh-server/" target="_blank" rel="noopener"
&gt;How to secure SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Now Fail2Ban&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I learned this by watching bad actors brute-force my server for days. Now they get five tries and a timeout. Yours should too.&lt;/p&gt;
&lt;p&gt;Next steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pair Fail2Ban with HTTPS and strong passwords&lt;/li&gt;
&lt;li&gt;Add a reverse proxy like NGINX or Caddy&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/fail2ban_hu_475b10a63613122f.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/fail2ban_hu_4912c86869461fba.webp" width="100" height="100" alt="Fail2Ban Official Wiki" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Fail2Ban Official Wiki&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://github.com/fail2ban/fail2ban/wiki"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://diymediaserver.com/images/logos/jellyfin.svg" alt="Jellyfin Official Documentation" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Jellyfin Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://jellyfin.org/docs/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;
Fail2Ban covers the application layer. This fanless N150 appliance covers the network layer in front of it, with six Intel i226-V 2.5GbE ports and hardware AES-NI for OPNsense or pfSense. Running both means a banned address gets dropped at the edge instead of reaching Jellyfin at all.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>DIYMEDIASERVER 2025 Year in Review</title><link>https://diymediaserver.com/post/2025-year-in-review/</link><pubDate>Sat, 27 Dec 2025 06:32:28 -0700</pubDate><guid>https://diymediaserver.com/post/2025-year-in-review/</guid><description>&lt;img src="https://diymediaserver.com/post/2025-year-in-review/featured_hu_f4e5f401d00eee3d.webp" alt="Featured image of post DIYMEDIASERVER 2025 Year in Review" /&gt;&lt;h2 id="looking-back-at-the-mess-we-started-with"&gt;Looking Back at the Mess We Started With
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s December 27th, and somehow we made it through our first year!&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been following along, you know this blog started as a therapy journal for the saga of fixing my media server that kept failing. One tower doing everything. ISP router I&amp;rsquo;d never logged into. Backups that were &amp;ldquo;I should really do that.&amp;rdquo; The classic homelab setup.&lt;/p&gt;
&lt;p&gt;2025 was the year I finally stopped duct-taping problems and built something that works. Four boxes with dedicated jobs. Proxmox running things properly. Docker that doesn&amp;rsquo;t make me want to throw my keyboard. And yeah, actual backups that saved my ass more than once.&lt;/p&gt;
&lt;p&gt;You asked the important questions. I burned my nights and weekends figuring shit out. Here&amp;rsquo;s a summary of what we accomplished in 2025.&lt;/p&gt;
&lt;h2 id="the-big-picture"&gt;The Big Picture
&lt;/h2&gt;&lt;p&gt;This year, DIYMediaServer turned into a complete blueprint for running your own media infrastructure. The whole thing: hardware, virtualization, networking, storage, automation, and the backup strategy that lets you experiment without worry. Not the &amp;ldquo;install Jellyfin and pray&amp;rdquo; tutorial.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re still running everything on one machine with an ISP router, this 2025 review is your roadmap out. No enterprise budget required. Better decisions and way fewer panic reboots during movie night.&lt;/p&gt;
&lt;h2 id="the-wins-what-got-published"&gt;The Wins: What Got Published
&lt;/h2&gt;&lt;p&gt;I wrote a lot this year. Maybe too much. But every post came from a real problem I hit or a question one of you asked.&lt;/p&gt;
&lt;h3 id="hardware-that-makes-sense"&gt;Hardware That Makes Sense
&lt;/h3&gt;&lt;p&gt;The four-box architecture was the turning point. Instead of one overworked machine doing everything badly, I split responsibilities:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Router&lt;/strong&gt; - Kicked the ISP router to modem-only duty and built a proper OPNsense box. VLANs, firewall rules, actual logs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The NAS&lt;/strong&gt; - Said goodbye to VM storage nightmares and USB drives hanging off Proxmox nodes. Dedicated hardware, proper HBAs, filesystems that won&amp;rsquo;t explode. My media library finally has a real and stable home.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Compute Node&lt;/strong&gt; - Apps got their own Proxmox box. One dead component used to take down everything. Not anymore. Compute crashes? Router and storage keep humming along.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Backup Server&lt;/strong&gt; - PBS finally got its own machine and became the &amp;ldquo;try anything without fear&amp;rdquo; box. Break something with an upgrade? Five-minute restore instead of rebuilding from scratch.&lt;/p&gt;
&lt;p&gt;These weren&amp;rsquo;t abstract posts. They&amp;rsquo;re the exact setup I&amp;rsquo;m running. Every gotcha I hit along the way is spelled out so you don&amp;rsquo;t have to step in them.&lt;/p&gt;
&lt;h3 id="dockers-comeback-tour"&gt;Docker&amp;rsquo;s Comeback Tour
&lt;/h3&gt;&lt;p&gt;Remember when I was all-in on LXCs? Yeah, about that.&lt;/p&gt;
&lt;p&gt;2025 was my public admission that I&amp;rsquo;d been fighting containers the wrong way. The LXC hell trilogy laid it all out: UID/GID nightmares, NFS permission walls, the &amp;ldquo;secure by default&amp;rdquo; trap that made simple media sharing an absolute nightmare.&lt;/p&gt;
&lt;p&gt;I crawled back to Docker for the media stack and documented the whole journey. The Arr suite comparison showed exactly why throwing Radarr, Sonarr, and friends into Docker Compose is better than individual LXCs. Cleaner updates. Simpler mounts. One export to storage and done.&lt;/p&gt;
&lt;p&gt;LXCs still have their place for lightweight services and utilities. But for media apps? Docker won, and I&amp;rsquo;m not too proud to say I was wrong.&lt;/p&gt;
&lt;p&gt;I might give LXCs another shot in 2026 with a different mounting method.&lt;/p&gt;
&lt;h3 id="automation-that-works"&gt;Automation That Works
&lt;/h3&gt;&lt;p&gt;Your media stack should work for you. This year we built out the full automation chain:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Download management&lt;/strong&gt;: SABnzbd, Radarr, Prowlarr guides that turn &amp;ldquo;I want this show&amp;rdquo; into &amp;ldquo;it&amp;rsquo;s already downloaded and sorted.&amp;rdquo; Complete with the Usenet vs torrents breakdown and SABnzbd vs NZBGet comparison.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Media processing&lt;/strong&gt;: Tdarr running in an unprivileged LXC with QuickSync, letting your Intel iGPU chew through transcodes while you do literally anything else.&lt;/p&gt;
&lt;p&gt;Profilarr will be covered in 2026 as an alternative to Tdarr.&lt;/p&gt;
&lt;h3 id="the-boring-stuff-that-saved-my-weekends"&gt;The Boring Stuff That Saved My Weekends
&lt;/h3&gt;&lt;p&gt;Some posts won&amp;rsquo;t get much traffic, but they quietly prevent disasters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-use-parted-to-create-partitions/" target="_blank" rel="noopener"
&gt;parted and mount points&lt;/a&gt; - So your drives don&amp;rsquo;t unmount on reboot&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/" target="_blank" rel="noopener"
&gt;UID/GID mapping in Proxmox&lt;/a&gt; - Ending the &amp;ldquo;Permission denied&amp;rdquo; screaming sessions&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/hardware-why-you-need-an-hba/" target="_blank" rel="noopener"
&gt;HBA guide&lt;/a&gt; - Why cheap SATA cards will ruin your ZFS pool&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/upgrade-debian-12-to-13-proxmox-lxc-243-credentials-fix/" target="_blank" rel="noopener"
&gt;Debian 12 to 13 upgrade fix&lt;/a&gt; - The systemd credential bug that hit everyone on Proxmox 9&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/complete-guide-securing-ssh-server/" target="_blank" rel="noopener"
&gt;SSH hardening&lt;/a&gt; - Stop leaving root login wide open&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-i-fixed-my-24-hour-nfs-crash-loop-with-mergerfs-lxc-and-proxmox/" target="_blank" rel="noopener"
&gt;The 24-hour NFS crash loop story&lt;/a&gt; - A debugging nightmare with MergerFS that taught me more about the kernel than I wanted to know&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each one small. Together? They turn &amp;ldquo;mostly works&amp;rdquo; into &amp;ldquo;boringly reliable.&amp;rdquo;&lt;/p&gt;
&lt;h3 id="network-and-security-upgrades"&gt;Network and Security Upgrades
&lt;/h3&gt;&lt;p&gt;The networking stack finally grew up:&lt;/p&gt;
&lt;p&gt;Proper routing with OPNsense. VLANs separating family devices from the media stack from random IoT garbage. Pi-hole v6 on multiple nodes, synced with Nebula so your DNS stays consistent.&lt;/p&gt;
&lt;p&gt;Not sexy stuff. But it&amp;rsquo;s the difference between &amp;ldquo;my Smart-Switch can somehow access my NAS shares&amp;rdquo; and &amp;ldquo;everything is where it should be and logged.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="whats-missing"&gt;What&amp;rsquo;s Missing?
&lt;/h2&gt;&lt;p&gt;Let me be honest about my documentation gaps, because 2026 needs to address them:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remote access&lt;/strong&gt; - I&amp;rsquo;ve barely touched this. You&amp;rsquo;ve got a great local setup, but accessing your media from outside your network safely? That&amp;rsquo;s the next frontier. I picked up a cheap VPS during Black Friday sales, and I&amp;rsquo;m building out a multi-part series on using it as a secure front door. Caddy for reverse proxy, Wireguard to tunnel back home, keeping your services and ISP IP hidden behind the VPS. No exposing ports directly. No hoping your ISP doesn&amp;rsquo;t change your IP. Actual security with the flexibility to access everything remotely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS and MergerFS don&amp;rsquo;t play nice&lt;/strong&gt; - The 24-hour crash loop story hinted at this, but there&amp;rsquo;s a bigger problem. NFS and MergerFS together create weird edge cases and performance issues I&amp;rsquo;ve been working around instead of fixing. Early 2026, I&amp;rsquo;m diving into VirtioFS as an alternative and rethinking how storage gets shared between Proxmox hosts and VMs. This one&amp;rsquo;s going to be a deep dive with real testing. I&amp;rsquo;ll be testing an alternative mounting method for MergerFS as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring and alerting&lt;/strong&gt; - The stack is more stable now, but you&amp;rsquo;re still flying blind. When did that disk start filling up? Is Jellyfin actually responsive or only running? Uptime Kuma, Discord Alerts, Bash scripts, and simple health checks need to be covered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing and Configuring Proxmox&lt;/strong&gt; - I&amp;rsquo;ve talked about how I use Proxmox, but I haven&amp;rsquo;t documented how to install or configure it yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware deep dives&lt;/strong&gt; - The guides covered what to buy, but not enough on &lt;em&gt;why&lt;/em&gt;. Power efficiency numbers. Noise levels. Heat management. Budgets from &amp;ldquo;I have $500&amp;rdquo; to &amp;ldquo;I can spend $2000.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recovery scenarios&lt;/strong&gt; - PBS is set up, but I haven&amp;rsquo;t documented what a full restore looks like. Or migrating everything to new hardware.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &amp;ldquo;complete stack&amp;rdquo; templates&lt;/strong&gt; - I haven&amp;rsquo;t been great at creating good copy-and-paste solutions. That&amp;rsquo;s intentional. I want you to learn, not blindly paste. But some of you have asked for full &amp;ldquo;recipe&amp;rdquo; builds that are copy, paste, adjust to taste.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More debugging stories&lt;/strong&gt; - I will be focusing on more real problems. The weird bugs. More Lightbulb moments documented. The &amp;ldquo;here&amp;rsquo;s what I tried that didn&amp;rsquo;t work before I found the solution.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="where-you-might-be-right-now"&gt;Where You Might Be Right Now
&lt;/h2&gt;&lt;h3 id="everythings-still-in-one-box"&gt;&amp;ldquo;Everything&amp;rsquo;s still in one box&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Start with the four-box hardware guide. Move routing off first, then storage. You don&amp;rsquo;t need to do it all at once, but every piece you split out makes the whole system more resilient.&lt;/p&gt;
&lt;h3 id="lxc-permissions-are-killing-me"&gt;&amp;ldquo;LXC permissions are killing me&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;The LXC hell series is literally your story. Switch media apps to Docker, keep LXCs for the lightweight stuff. Your blood pressure will thank you.&lt;/p&gt;
&lt;h3 id="backups-theres-a-usb-drive-somewhere"&gt;&amp;ldquo;Backups? There&amp;rsquo;s a USB drive somewhere&amp;hellip;&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;PBS guide. Find a cheap mini PC, add a big drive, let Proxmox handle it. The first time you restore instead of rebuild, you&amp;rsquo;ll wonder why you waited.&lt;/p&gt;
&lt;h3 id="my-isp-router-works"&gt;&amp;ldquo;My ISP router&amp;hellip; works?&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;OPNsense build. One weekend. You&amp;rsquo;ll immediately see the difference when you can control what&amp;rsquo;s happening on your network.&lt;/p&gt;
&lt;h3 id="im-new-and-this-is-overwhelming"&gt;&amp;ldquo;I&amp;rsquo;m new and this is overwhelming&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Start small. Pick the SSH hardening guide. Get one Arr app running. You don&amp;rsquo;t need the full four-box dream build on day one. Build confidence with small wins, then come back for the architecture.&lt;/p&gt;
&lt;h2 id="before-and-after"&gt;Before and After
&lt;/h2&gt;&lt;p&gt;I want to be clear about what changed over the last year:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;: One box doing everything. LXCs everywhere with permission nightmares. VM storage on a prayer. ISP router mystery settings. &amp;ldquo;Backups&amp;rdquo; = good intentions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now&lt;/strong&gt;: Dedicated router, NAS, compute, and backup boxes. Proxmox with clear roles. Docker for media apps. Automated downloads with Radarr and SABnzbd. OPNsense with VLANs. PBS with actual tested restores.&lt;/p&gt;
&lt;p&gt;Your homelab shouldn&amp;rsquo;t be a second job. It should work quietly in the background and let you &lt;em&gt;use&lt;/em&gt; your media instead of constantly fixing things. Don&amp;rsquo;t get me wrong, tinkering is still fun. But I want to pick when I tinker instead of getting dragged into it mid-movie.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;p&gt;2025 was foundations. Getting the architecture right, escaping container hell, setting up backups, and basic networking.&lt;/p&gt;
&lt;p&gt;2026 is where we go deeper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remote access allowing sharing with friends&lt;/li&gt;
&lt;li&gt;Monitoring and alerting that helps&lt;/li&gt;
&lt;li&gt;Complete stack templates you can copy&lt;/li&gt;
&lt;li&gt;More budget vs performance breakdowns&lt;/li&gt;
&lt;li&gt;Disaster recovery walkthroughs&lt;/li&gt;
&lt;li&gt;Ansible Playbooks&lt;/li&gt;
&lt;li&gt;Better MergerFS setup&lt;/li&gt;
&lt;li&gt;More real debugging stories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This blog exists because I got tired of forum posts from 2015 that assumed you already knew everything. The goal is making this stuff accessible. Not dumbed down. Explained properly, with the context that matters.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also forcing me to keep my skills sharp and to keep learning.&lt;/p&gt;
&lt;h2 id="setup-one-thing-before-new-years"&gt;Setup One Thing Before New Year&amp;rsquo;s
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve got a week. Pick something from this year&amp;rsquo;s posts and do it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Move your routing to OPNsense&lt;/li&gt;
&lt;li&gt;Set up PBS&lt;/li&gt;
&lt;li&gt;Configure proper SSH hardening&lt;/li&gt;
&lt;li&gt;Get Radarr automating one library&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing. Build momentum. Start 2026 with a win instead of another &amp;ldquo;I really should do that.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re building infrastructure you understand and control. That&amp;rsquo;s worth the effort.&lt;/p&gt;
&lt;p&gt;Thanks for reading this year. See you in 2026.&lt;/p&gt;
&lt;p&gt;Hit me up on Reddit or E-mail if there&amp;rsquo;s something specific you want covered. I&amp;rsquo;m making the list now.&lt;/p&gt;</description></item><item><title>Sync Multiple Pi-hole Servers with Nebula Sync</title><link>https://diymediaserver.com/post/sync-multiple-pihole-servers-nebula-sync/</link><pubDate>Sat, 13 Dec 2025 07:32:28 -0700</pubDate><guid>https://diymediaserver.com/post/sync-multiple-pihole-servers-nebula-sync/</guid><description>&lt;img src="https://diymediaserver.com/post/sync-multiple-pihole-servers-nebula-sync/featured_hu_b34739235267d080.webp" alt="Featured image of post Sync Multiple Pi-hole Servers with Nebula Sync" /&gt;&lt;p&gt;You add a blocklist to your first Pi-hole. Easy. Then you remember you&amp;rsquo;ve got two more Pi-holes scattered around your network. Each one needs the same update. Manually. You tab between admin pages, copying settings, and by the second one you&amp;rsquo;re asking yourself why you ever thought multiple Pi-holes was a good idea.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/lovelaze/nebula-sync" target="_blank" rel="noopener"
&gt;Nebula Sync&lt;/a&gt; fixes this. It keeps multiple Pi-hole servers in sync by automatically copying blocklists, local DNS records, and settings from one primary Pi-hole to all your replicas. Change something once on the primary, and Nebula pushes it everywhere else within a few minutes.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running multiple Pi-holes and you&amp;rsquo;re tired of manual updates, this guide will get you set up. If you can log into &lt;code&gt;pihole/admin&lt;/code&gt; and run Docker, you can do this.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Run multiple Pi-hole servers for redundancy, designate one as primary, and use Nebula Sync to automatically replicate blocklists, local DNS, and settings to all replicas without manual updates.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Quick checklist&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 2+ working Pi-hole nodes (same major version, ideally v6)&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; One node chosen as &lt;strong&gt;PRIMARY&lt;/strong&gt; (the only one you edit)&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; A basic understanding of Docker/Compose&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Router/DHCP hands out both Pi-hole IPs (DNS) for failover&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-sync-multiple-pi-hole-servers"&gt;Why Sync Multiple Pi-hole Servers?
&lt;/h2&gt;&lt;h3 id="why-run-more-than-one-pi-hole"&gt;Why Run More Than One Pi-hole?
&lt;/h3&gt;&lt;p&gt;One Pi-hole works great until it doesn&amp;rsquo;t. Maybe you&amp;rsquo;re updating it and something breaks. Maybe the SD card dies. Whatever the reason, when your single DNS server goes down, your entire network loses internet. Your spouse can&amp;rsquo;t stream. The smart home stops working. You know how this goes.&lt;/p&gt;
&lt;p&gt;Multiple Pi-holes solve this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redundancy&lt;/strong&gt;: One Pi-hole fails, DNS keeps working.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance without downtime&lt;/strong&gt;: Update one node while the other handles queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better coverage&lt;/strong&gt;: Every device stays protected during updates or failures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem? Managing them manually is a pain. Every blocklist or whitelist addition, every DNS entry, every setting tweak has to be repeated on each node. That gets old fast.&lt;/p&gt;
&lt;h3 id="two-sync-approaches-one-clear-winner-for-v6"&gt;Two Sync Approaches, One Clear Winner for v6
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;ve got two main options for syncing Pi-holes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gravity Sync&lt;/strong&gt; is the old reliable. It&amp;rsquo;s a bash script that syncs databases over SSH. Works great for Pi-hole v5.x, but it wasn&amp;rsquo;t built for v6&amp;rsquo;s API architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nebula Sync&lt;/strong&gt; is newer and designed specifically for Pi-hole v6&amp;rsquo;s web API. It syncs more than gravity databases. Local DNS records, group settings, and other v6 config travel with it.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re using Nebula Sync here because it&amp;rsquo;s built for v6 and works cleaner with current Pi-hole versions.&lt;/p&gt;
&lt;h2 id="prerequisites-and-lab-layout"&gt;Prerequisites and Lab Layout
&lt;/h2&gt;&lt;h3 id="what-you-need"&gt;What You Need
&lt;/h3&gt;&lt;p&gt;Before you start, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two or more working Pi-hole servers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Each Pi-hole accessible at &lt;code&gt;http://&amp;lt;ip&amp;gt;/admin&lt;/code&gt; or &lt;code&gt;https://&amp;lt;host&amp;gt;/admin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;admin password&lt;/strong&gt; set on each Pi-hole&lt;/li&gt;
&lt;li&gt;Something that can run Docker (often one of the Pi-holes works fine)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the Pi-hole nodes themselves, a Raspberry Pi 4 is the classic pick. Cheap, silent, and more than enough horsepower for DNS plus a Nebula Sync container.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB - Must have for this build.&lt;/strong&gt;&lt;br&gt;
Quiet, cheap, and plenty for Pi-hole + Nebula Sync.&lt;br&gt;
If you’re doing heavy DNS for a big network, use a small x86 box instead.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="example-setup"&gt;Example Setup
&lt;/h3&gt;&lt;p&gt;Throughout this guide, I&amp;rsquo;ll use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pihole1&lt;/code&gt; at 192.168.1.10 (Primary)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pihole2&lt;/code&gt; at 192.168.1.11 (Replica)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Swap in your own IPs. You can add more replicas later.&lt;/p&gt;
&lt;h3 id="check-pi-hole-versions-important"&gt;Check Pi-hole Versions (Important)
&lt;/h3&gt;&lt;p&gt;Log into each &lt;code&gt;pihole/admin&lt;/code&gt; dashboard and check the version at the bottom of the page.&lt;/p&gt;
&lt;p&gt;Keep all nodes on the same major version, ideally v6.x. Mixing versions causes weird issues. If they don&amp;rsquo;t match, update each node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pihole -up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="decide-where-nebula-sync-will-run"&gt;Decide Where Nebula Sync Will Run
&lt;/h3&gt;&lt;p&gt;Two common options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Option A (simplest)&lt;/strong&gt;: Run Nebula Sync as a Docker container on the primary Pi-hole.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option B&lt;/strong&gt;: Run it on a separate Docker host that can reach all Pi-hole admin URLs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most people, Option A is easiest.&lt;/p&gt;
&lt;h2 id="step-1-verify-each-pi-hole-works-standalone"&gt;Step 1: Verify Each Pi-hole Works Standalone
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t skip this. If a Pi-hole is broken before you sync it, you&amp;rsquo;ll spread the brokenness around.&lt;/p&gt;
&lt;p&gt;On &lt;strong&gt;each&lt;/strong&gt; Pi-hole:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://&amp;lt;pihole-ip&amp;gt;/admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Log in with the admin password.&lt;/li&gt;
&lt;li&gt;Confirm the dashboard loads and shows recent queries.&lt;/li&gt;
&lt;li&gt;From the terminal, tail logs:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pihole -t
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="5"&gt;
&lt;li&gt;From a client device, temporarily set DNS to only that Pi-hole and browse a few sites.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If DNS queries show up in the log, you&amp;rsquo;re good. If not, fix networking or service issues before moving on.&lt;/p&gt;
&lt;h2 id="step-2-gather-urls-and-credentials"&gt;Step 2: Gather URLs and Credentials
&lt;/h2&gt;&lt;p&gt;Nebula Sync talks to Pi-hole via the web API, so you need the exact URLs and passwords.&lt;/p&gt;
&lt;p&gt;For each Pi-hole, write down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full admin URL, including &lt;code&gt;/admin&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Example: &lt;code&gt;http://192.168.1.10/admin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Admin password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nebula expects this format:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;URL|password
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;http://192.168.1.10/admin|SuperSecretPassword
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
About 80% of sync failures happen because someone forgot &lt;code&gt;/admin&lt;/code&gt; at the end of the URL. Don&amp;rsquo;t be that person.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-3-create-a-nebula-sync-docker-compose-file"&gt;Step 3: Create a Nebula Sync Docker Compose File
&lt;/h2&gt;&lt;p&gt;Most people deploy Nebula Sync with Docker Compose or Portainer.&lt;/p&gt;
&lt;h3 id="example-composeyml"&gt;Example &lt;code&gt;compose.yml&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nebula-sync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/lovelaze/nebula-sync:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nebula-sync&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PRIMARY=http://192.168.1.10/admin|PRIMARY_PASSWORD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;REPLICAS=http://192.168.1.11/admin|REPLICA_PASSWORD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FULL_SYNC=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;RUN_GRAVITY=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;CRON=*/5 * * * *&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the URLs and passwords with your actual ones.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Putting passwords directly in the compose file works but isn&amp;rsquo;t great. For better security, use a &lt;code&gt;.env&lt;/code&gt; file:
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;From the same directory where your &lt;code&gt;compose.yaml&lt;/code&gt; file lives, create a &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano .env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PIHOLE_PRIMARY_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://192.168.1.10/admin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PIHOLE_PRIMARY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YourSecretPassword
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PIHOLE_SECONDARY_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://192.168.1.11/admin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PIHOLE_SECONDARY_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YourSecretPassword
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then reference them in compose like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nv"&gt;PRIMARY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIHOLE_PRIMARY_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIHOLE_PRIMARY_PASSWORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nv"&gt;REPLICAS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIHOLE_SECONDARY_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIHOLE_SECONDARY_PASSWORD&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you can share your compose file without scrubbing the password every time.&lt;/p&gt;
&lt;h3 id="example-composeyaml-using-the-env-file"&gt;Example &lt;code&gt;compose.yaml&lt;/code&gt; Using the .env File
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nebula-sync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/lovelaze/nebula-sync:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nebula-sync&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PRIMARY=${PIHOLE_PRIMARY_URL}|${PIHOLE_PRIMARY_PASSWORD}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;REPLICAS=${PIHOLE_SECONDARY_URL}|${PIHOLE_SECONDARY_PASSWORD}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;FULL_SYNC=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;RUN_GRAVITY=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;CRON=*/5 * * * *&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="key-environment-variables-explained"&gt;Key Environment Variables Explained
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PRIMARY&lt;/strong&gt;: The Pi-hole you&amp;rsquo;ll always edit manually. This is your source of truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REPLICAS&lt;/strong&gt;: Pi-holes that receive config from the primary. Separate multiple with spaces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FULL_SYNC&lt;/strong&gt;: Set to &lt;code&gt;true&lt;/code&gt; to sync everything Nebula supports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RUN_GRAVITY&lt;/strong&gt;: Tells Pi-hole to run gravity after syncing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRON&lt;/strong&gt;: Cron-style schedule. &lt;code&gt;*/5 * * * *&lt;/code&gt; means every 5 minutes, a good starting point.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Common mistakes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing &lt;code&gt;/admin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wrong &lt;code&gt;http://&lt;/code&gt; vs &lt;code&gt;https://&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Typos in passwords&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-4-deploy-nebula-sync"&gt;Step 4: Deploy Nebula Sync
&lt;/h2&gt;&lt;p&gt;From the directory containing your compose file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then check logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs -f nebula-sync
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What you want to see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Successful connection to primary&lt;/li&gt;
&lt;li&gt;Sync operations targeting replicas&lt;/li&gt;
&lt;li&gt;No authentication or connection errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you see authentication errors, double-check those URLs and passwords. Connection errors? Verify HTTP vs HTTPS and make sure you can load the admin pages in a browser from the Docker host.&lt;/p&gt;
&lt;h2 id="step-5-first-sync-test-with-gravity-and-blocklists"&gt;Step 5: First Sync Test with Gravity and Blocklists
&lt;/h2&gt;&lt;p&gt;Time to confirm real data is syncing.&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;primary Pi-hole&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Group Management &amp;gt; Adlists&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add a test blocklist or toggle an existing one.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save and Update&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Wait for the next Nebula run (or restart the container because who has patience?).&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;replica&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;/admin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Group Management &amp;gt; Adlists&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the new list appears and domain counts match closely.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If it&amp;rsquo;s there, gravity syncing works. Nice.&lt;/p&gt;
&lt;h2 id="step-6-sync-local-dns-and-custom-entries"&gt;Step 6: Sync Local DNS and Custom Entries
&lt;/h2&gt;&lt;p&gt;This is where Nebula Sync really shines compared to older tools. Custom DNS entries sync automatically.&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;primary&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Local DNS &amp;gt; DNS Records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add:
&lt;ul&gt;
&lt;li&gt;Hostname: &lt;code&gt;tim.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IP: &lt;code&gt;10.0.0.50&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After the next sync:&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;replica&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Local DNS &amp;gt; DNS Records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;tim.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm it exists and points to the same IP.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Test it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nslookup tim.example.com 192.168.1.11
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should get &lt;code&gt;10.0.0.50&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="step-7-day-to-day-workflow-primaryreplica-rules"&gt;Step 7: Day-to-Day Workflow (Primary/Replica Rules)
&lt;/h2&gt;&lt;p&gt;Once everything&amp;rsquo;s working, the rules are simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always&lt;/strong&gt; make changes on the primary Pi-hole.&lt;/li&gt;
&lt;li&gt;Treat replicas as &lt;strong&gt;read-only&lt;/strong&gt; in the UI.&lt;/li&gt;
&lt;li&gt;Let Nebula Sync handle propagation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can adjust the sync frequency if needed. For most homelabs, every 5 to 15 minutes works well. More frequent syncing means less drift time but more API calls hitting your Pi-holes.&lt;/p&gt;
&lt;h2 id="step-8-router-and-client-dns-configuration"&gt;Step 8: Router and Client DNS Configuration
&lt;/h2&gt;&lt;p&gt;To benefit from multiple Pi-holes, clients need to know about them.&lt;/p&gt;
&lt;h3 id="simple-setup-two-dns-ips"&gt;Simple Setup: Two DNS IPs
&lt;/h3&gt;&lt;p&gt;In your router&amp;rsquo;s DHCP settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS 1: 192.168.1.10&lt;/li&gt;
&lt;li&gt;DNS 2: 192.168.1.11&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clients automatically fail over if one Pi-hole is unavailable. It&amp;rsquo;s not instant, but it works.&lt;/p&gt;
&lt;h3 id="advanced-option-virtual-ip"&gt;Advanced Option: Virtual IP
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;re feeling ambitious, tools like keepalived can give you a single virtual IP that floats between Pi-holes. This is optional and overkill for most people.&lt;/p&gt;
&lt;h2 id="step-9-testing-failover-and-basic-pi-hole-tests"&gt;Step 9: Testing Failover and Basic Pi-hole Tests
&lt;/h2&gt;&lt;p&gt;Make sure your redundancy actually works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ensure clients receive both DNS IPs from DHCP.&lt;/li&gt;
&lt;li&gt;Stop Pi-hole on the primary or reboot the server.&lt;/li&gt;
&lt;li&gt;Browse the web from a client.&lt;/li&gt;
&lt;li&gt;Check the replica&amp;rsquo;s &lt;strong&gt;Query Log&lt;/strong&gt; for activity.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Useful commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pihole -t&lt;/code&gt; to watch live queries&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dig&lt;/code&gt; or &lt;code&gt;nslookup&lt;/code&gt; against each Pi-hole IP&lt;/li&gt;
&lt;li&gt;Visit ad-heavy sites and confirm blocks appear&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-10-maintenance-and-updates"&gt;Step 10: Maintenance and Updates
&lt;/h2&gt;&lt;h3 id="updating-pi-hole"&gt;Updating Pi-hole
&lt;/h3&gt;&lt;p&gt;On each node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pihole -up
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Keep versions aligned to avoid API mismatches.&lt;/p&gt;
&lt;h3 id="updating-nebula-sync"&gt;Updating Nebula Sync
&lt;/h3&gt;&lt;p&gt;From the compose directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="backup-strategy"&gt;Backup Strategy
&lt;/h3&gt;&lt;p&gt;Before major changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Export Pi-hole config from the primary&lt;/li&gt;
&lt;li&gt;Keep a copy offline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If something breaks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Stop the Nebula container.&lt;/li&gt;
&lt;li&gt;Restore the primary from backup.&lt;/li&gt;
&lt;li&gt;Restart Nebula Sync.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="troubleshooting-common-problems"&gt;Troubleshooting Common Problems
&lt;/h2&gt;&lt;h3 id="quick-troubleshooting-table"&gt;Quick Troubleshooting Table
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication errors in Nebula logs&lt;/td&gt;
&lt;td&gt;Wrong password or URL format&lt;/td&gt;
&lt;td&gt;Re-check password and make sure the URL includes &lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection errors / timeouts&lt;/td&gt;
&lt;td&gt;Wrong scheme (HTTP vs HTTPS) or network issues&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;http://&lt;/code&gt; vs &lt;code&gt;https://&lt;/code&gt;, check firewalls, make sure the admin UI loads in a browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changes not appearing on replicas&lt;/td&gt;
&lt;td&gt;FULL_SYNC disabled, schedule not running, or edits made on replica&lt;/td&gt;
&lt;td&gt;Confirm &lt;code&gt;FULL_SYNC=true&lt;/code&gt;, check logs for runs, only edit the primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nodes drift after updates&lt;/td&gt;
&lt;td&gt;Mixed major versions&lt;/td&gt;
&lt;td&gt;Upgrade all nodes; don&amp;rsquo;t mix v5 and v6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expecting DHCP to sync&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Keep DHCP on router or manage separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="nebula-sync-fails-authentication"&gt;Nebula Sync Fails Authentication
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Double-check admin passwords.&lt;/li&gt;
&lt;li&gt;Confirm &lt;code&gt;/admin&lt;/code&gt; is included in the URL.&lt;/li&gt;
&lt;li&gt;Verify HTTP vs HTTPS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="changes-not-appearing-on-replicas"&gt;Changes Not Appearing on Replicas
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Confirm &lt;code&gt;FULL_SYNC=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check Nebula logs for recent runs.&lt;/li&gt;
&lt;li&gt;Make sure changes were made on the primary only.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mixed-pi-hole-versions"&gt;Mixed Pi-hole Versions
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Upgrade all nodes to the same major version.&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t mix v5 and v6.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="dhcp-expectations"&gt;DHCP Expectations
&lt;/h3&gt;&lt;p&gt;Nebula Sync does &lt;strong&gt;not&lt;/strong&gt; sync DHCP settings or leases. Keep DHCP on your router or manage it separately.&lt;/p&gt;
&lt;p&gt;If your Pi is starting to choke on a busy network or you want to run more containers alongside Nebula Sync, a small x86 mini-PC is the next step up.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C4GWPYMB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/beelink_hu_5974274794f3200a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/beelink_hu_dd7f73b5fd08d717.webp" width="600" height="481" alt="Beelink SER5 (Ryzen 5 5500U)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Beelink SER5 (Ryzen 5 5500U) Nice to have but not required.&lt;/strong&gt;&lt;br&gt;
Best for people upgrading from a Raspberry Pi who want Pi-hole plus extra Docker containers without lag, but it costs more and uses more power than a Pi.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4fB1X31" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="faqs-nebula-sync--pi-hole"&gt;FAQs (Nebula Sync + Pi-hole)
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I sync more than two Pi-hole servers with Nebula Sync?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;Yes. Define one primary and add multiple replicas in the &lt;code&gt;REPLICAS&lt;/code&gt; variable.
Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="l"&gt;REPLICAS=http://ph2.example.com|password,http://ph3.example.com|password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or with env:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="l"&gt;REPLICAS=${PIHOLE_SECONDARY_URL}|${PIHOLE_SECONDARY_PASSWORD},${PIHOLE_TERTIARY_URL}|${PIHOLE_TERTIARY_PASSWORD}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I have to run Nebula Sync in Docker?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Docker is the most common and easiest approach for updates and isolation.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does Nebula Sync work with Pi-hole v6?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. It&amp;rsquo;s designed around Pi-hole v6&amp;rsquo;s web API and HTTPS admin interface.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will Nebula Sync keep DHCP settings in sync?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. DHCP settings and leases are node-specific and don&amp;rsquo;t sync.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I edit settings directly on replicas?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;You can, but Nebula will overwrite them on the next run. Save yourself the headache and only edit the primary.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What happens if a replica is offline during sync?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Nebula will fail that run and catch up when the replica comes back online.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is it safe to mix HTTP and HTTPS?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, as long as each Pi-hole is defined with the correct scheme in Nebula&amp;rsquo;s config.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How often should I run the sync?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Every 5 to 15 minutes is typical for homelabs.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I still need to run `pihole -g` on replicas?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Run it on the primary only. Nebula syncs the results.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I test that everything works?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Add a test DNS record or blocklist on the primary and confirm it appears and functions on replicas.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Running multiple Pi-hole servers for redundancy is great until you realize you&amp;rsquo;ve tripled your management overhead. Nebula Sync fixes this by letting you treat one Pi-hole as the source of truth and having the rest automatically mirror it.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re tired of juggling multiple &lt;code&gt;pihole/admin&lt;/code&gt; pages and trying to remember which server has the latest config, this setup changes everything. Start with two nodes, get comfortable with the workflow, then add more as you need them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next steps&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Experiment with sync schedules&lt;/li&gt;
&lt;li&gt;Add a third replica&lt;/li&gt;
&lt;li&gt;Explore virtual IP failover if you want true high availability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&amp;rsquo;ve got this running, you won&amp;rsquo;t go back to manual Pi-hole updates.&lt;/p&gt;
&lt;p&gt;If Pi-hole sync is a stepping stone into a bigger homelab and you want room for Proxmox, VMs, and serious networking, the MS-01 is a different class of machine.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Optional upgrade.&lt;/strong&gt;&lt;br&gt;
Best for stepping up from Pi-hole into a Proxmox mini-lab with serious networking thanks to built-in 10GbE and 2.5GbE, but it’s unnecessary overkill if you’re only doing simple Pi-hole sync.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Best Proxmox Backup Server Setup (2026): PBS Configuration</title><link>https://diymediaserver.com/post/media-server-backup-2025/</link><pubDate>Mon, 01 Dec 2025 05:04:02 -0600</pubDate><guid>https://diymediaserver.com/post/media-server-backup-2025/</guid><description>&lt;img src="https://diymediaserver.com/post/media-server-backup-2025/featured_hu_250d96294577ff11.webp" alt="Featured image of post Best Proxmox Backup Server Setup (2026): PBS Configuration" /&gt;&lt;p&gt;I deleted my entire photo library.&lt;/p&gt;
&lt;p&gt;~40,000 photos. Years of memories. Gone in the time it takes to hit enter.&lt;/p&gt;
&lt;p&gt;I was in the Proxmox shell, typing a command to delete an LXC I didn&amp;rsquo;t need anymore. I fat-fingered the ID. The wrong container vanished. My Immich server. The thing that held every family photo, every vacation, every moment my wife cared about preserving.&lt;/p&gt;
&lt;p&gt;I knew what I did the instant I hit enter. That sick feeling in your stomach when you realize you wiped something that matters.&lt;/p&gt;
&lt;p&gt;The photos themselves were fine. I had those backed up separately. But the albums? The organization? The metadata? The hundreds of hours my wife spent sorting and curating everything? That was gone.&lt;/p&gt;
&lt;p&gt;It took several days of painstaking work to put it all back. Re-creating albums. Re-adding photos to albums. My wife&amp;rsquo;s reaction was, let&amp;rsquo;s say, worse than when the internet went down for a few hours.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when I stopped telling myself I&amp;rsquo;d set up proper backups &amp;ldquo;eventually.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I pulled an old DOGE mining rig from 2013 out of the closet, bought a cheap 2U case, and had Proxmox Backup Server running a few hours later.&lt;/p&gt;
&lt;p&gt;Now when things break, I don&amp;rsquo;t worry about it. I restore the backup that&amp;rsquo;s always less than 24 hours old and move on.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
ZFS, RAID, and snapshots are not backups. A dedicated Proxmox Backup Server gives you image based backups of your VMs, LXCs, and configs on a separate box. That means you can break your Proxmox compute node, or even replace it, and bring everything back in minutes instead of days.
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B07H289S7C"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate-8tb_hu_3ac46966354d0ce6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate-8tb_hu_892fe1aa3bbbb134.webp" width="600" height="861" alt="Seagate BarraCuda Internal Hard Drive 8TB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate BarraCuda Internal Hard Drive 8TB&lt;/strong&gt;
Right now one of the best price per GB you can find. Also has a 2-year warranty.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/45Cw97V" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="everyone-talks-about-backups-almost-nobody-has-them"&gt;Everyone Talks About Backups. Almost Nobody Has Them.
&lt;/h2&gt;&lt;p&gt;You tell yourself the same thing everyone else does:
&amp;ldquo;I have ZFS, I am fine.&amp;rdquo;
&amp;ldquo;I have RAID, I am fine.&amp;rdquo;
&amp;ldquo;I have snapshots, I am fine.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Before I built my backup server, I lost data more than once. I broke VMs trying to optimize things. I had to rebuild LXCs from scratch because I thought I was careful enough to not need backups. Then I thought &amp;ldquo;I use Ansible playbooks and I can reconfigure services quickly&amp;rdquo;, but Ansible doesn&amp;rsquo;t restore your actual data.&lt;/p&gt;
&lt;p&gt;My all-in-one server&amp;rsquo;s PSU died while I was at work. It took out the router, storage, and compute all at once. When it was fixed, two VMs wouldn&amp;rsquo;t boot. Likely corruption from the sudden power loss.&lt;/p&gt;
&lt;p&gt;But I still didn&amp;rsquo;t build a backup server. Adding another box meant:
More power draw.
More complexity.
More things to maintain and update.&lt;/p&gt;
&lt;p&gt;Then I deleted Immich.&lt;/p&gt;
&lt;p&gt;That was the line.&lt;/p&gt;
&lt;h2 id="what-proxmox-backup-server-is"&gt;What Proxmox Backup Server Is
&lt;/h2&gt;&lt;p&gt;Proxmox Backup Server, or PBS, is not a fancy rsync script. It is a dedicated backup appliance for Proxmox.&lt;/p&gt;
&lt;p&gt;It does image level backups of LXCs and VMs. That means it backs up the whole guest, not a handful of config files you hope you got right. When you restore, you&amp;rsquo;re putting the entire VM or container back exactly how it was.&lt;/p&gt;
&lt;p&gt;Core PBS features you care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deduplication&lt;/strong&gt;
PBS breaks data into chunks and reuses them across backups. Ten backups of the same VM do not use ten times the space.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Incremental backups&lt;/strong&gt;
The first backup is large. After that, PBS only stores the changes. That means daily backups are actually realistic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retention and pruning&lt;/strong&gt;
You can set rules like &amp;ldquo;keep 7 daily, 4 weekly, 6 monthly&amp;rdquo; and PBS will handle cleanup.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;
PBS can verify that backups are readable. That is the part most people skip until it is too late.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Encryption, if you want it&lt;/strong&gt;
Helpful if the box is in a location you do not fully trust.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Short version. PBS is the ctrl-z for your Proxmox stack.&lt;/p&gt;
&lt;h2 id="why-the-backup-node-needs-its-own-box"&gt;Why The Backup Node Needs Its Own Box
&lt;/h2&gt;&lt;p&gt;If you run PBS on the same hardware you are backing up, you are not doing backups. You are making local copies and hoping the box never dies.&lt;/p&gt;
&lt;p&gt;Backups should live on different hardware. Period.&lt;/p&gt;
&lt;p&gt;Here is why PBS needs to be a separate node and not a VM sharing hardware with everything else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware failure isolation&lt;/strong&gt;
If your compute node dies, you still have PBS. You can rebuild a fresh Proxmox host, reconnect it to PBS, and start restoring guests. If PBS lived on that same dead box, you would be staring at a pile of useless backups on disks you cannot even boot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NAS failure isolation&lt;/strong&gt;
Same story for storage. If your NAS dies and PBS is another datastore living on that NAS, you lose both the source and the backups in one shot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security isolation&lt;/strong&gt;
If something nasty gets into your compute node or NAS, it should not be able to casually wipe your backups. PBS on its own box, with good firewall rules, is much harder to wipe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Upgrade and experiment freedom&lt;/strong&gt;
You want to be able to reinstall Proxmox on the compute node without touching PBS. You want to be able to test new Proxmox versions, new kernels, new storage layouts. That is only safe if your backups live somewhere else.&lt;/p&gt;
&lt;p&gt;One simple rule:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If the thing you are backing up and the backup live on the same physical box, you do not have a backup. You have a copy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="how-i-finally-built-this-thing"&gt;How I Finally Built This Thing
&lt;/h2&gt;&lt;p&gt;After the Immich incident, I was done making excuses.&lt;/p&gt;
&lt;p&gt;I had an old DOGE mining rig from 2013 sitting in a closet. Yes, a DOGE mining rig. That hardware bought during the peak of meme cryptocurrency mania was about to become the most critical piece of infrastructure in my homelab.&lt;/p&gt;
&lt;p&gt;Intel G1610 CPU.
GIGABYTE GA-B75M-D3H motherboard.
16GB of RAM that&amp;rsquo;s probably worth more than the entire rig today (thanks, AI).&lt;/p&gt;
&lt;p&gt;It never made me any money mining Dogecoin. But it was about to save my ass repeatedly.&lt;/p&gt;
&lt;p&gt;I bought a cheap 2U Rosewill case and moved everything over. A few hours later, PBS was running.&lt;/p&gt;
&lt;p&gt;I threw in a 128GB SSD for the PBS system and 2 4TB HDDs for backup storage. That was it.
Total cost: ~$100 for the case.
I had some old drives on hand for the OS and storage.&lt;/p&gt;
&lt;p&gt;Now that ancient hardware backs up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8 VMs&lt;/li&gt;
&lt;li&gt;12 LXCs&lt;/li&gt;
&lt;li&gt;Everything that matters in my homelab&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most guests get nightly backups. A few less critical ones get weekly backups. I keep 4 nightly backups, one monthly, then everything older gets auto pruned. And thanks to deduplication, I am only using 12% of the available storage.&lt;/p&gt;
&lt;p&gt;If a CPU that once supported mining joke cryptocurrency can handle PBS for 20 guests, you do not need to overthink your hardware.&lt;/p&gt;
&lt;p&gt;The first thing I did after getting PBS up and running was testing a restore. I picked my backup Pi-hole server since I run two and losing one wouldn&amp;rsquo;t matter. I backed it up. Then I deleted it on purpose.&lt;/p&gt;
&lt;p&gt;Five minutes later it was back. Running. Working. Like nothing happened.&lt;/p&gt;
&lt;p&gt;The relief I felt was immense. A weight I didn&amp;rsquo;t even realize I was carrying lifted off me. All those years of anxiety about breaking things. All those careful, tentative changes to production services. All that tiptoeing around my own infrastructure. Gone.&lt;/p&gt;
&lt;p&gt;This actually worked.&lt;/p&gt;
&lt;p&gt;I have restored several LXCs and VMs at least two dozen times since then. Testing migrations. Trying new features. Breaking things to see what happens. Every time it is five minutes to get back to working.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t have a rack and don&amp;rsquo;t want one? A cheap mini tower works fine for a PBS box. Same parts inside, same job done.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0B6382TTS"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/rosewill-helix_hu_5d1a3447e20888ac.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/rosewill-helix_hu_6d8d1b4c251f69f1.webp" width="600" height="550" alt="Rosewill FBM-X2-400-HELIX Micro ATX Tower" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Rosewill FBM-X2-400-HELIX&lt;/strong&gt;
Compact Micro ATX tower with a pre-installed 400 W PSU, room for multiple 2.5 and 3.5 inch drives, and enough airflow options for a tidy budget backup server build.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4pGVNQZ" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="how-pbs-backs-up-proxmox-better-than-your-scripts"&gt;How PBS Backs Up Proxmox Better Than Your Scripts
&lt;/h2&gt;&lt;p&gt;You can absolutely hack together backups with rsync, tar, and hope. You can write bash scripts that dump configs and copy directories.&lt;/p&gt;
&lt;p&gt;PBS is better for one reason. It speaks Proxmox.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PBS uses datastores&lt;/strong&gt;
You create one or more datastores on the PBS box. These are directories on disks, usually large HDDs or SSDs. Proxmox guests get backed up into those datastores.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backups are chunked and deduped&lt;/strong&gt;
PBS stores data in chunks. If several VMs or containers share the same data, PBS stores that data once and references it many times. This reduces space and makes incrementals light.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backups are incremental&lt;/strong&gt;
First backup is full. After that, PBS only needs changed chunks. You can run nightly backups without filling disks in a week.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backups are guest aware&lt;/strong&gt;
VM backups use snapshot mechanisms correctly. LXC backups include config and data in a consistent way. You are not racing writes like you might be with raw rsync.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Restores are simple&lt;/strong&gt;
From the Proxmox UI you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Restore a VM or LXC when you break it&lt;/li&gt;
&lt;li&gt;Restore to a new ID so you can test first&lt;/li&gt;
&lt;li&gt;Move a guest to a different node during restore (I use this a lot)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don&amp;rsquo;t need to hunt down that random blog post you followed three months ago. You don&amp;rsquo;t need to remember which flags you used or how you configured the service. PBS gives you the entire machine, exactly as it was before you broke it.&lt;/p&gt;
&lt;h2 id="what-you-should-back-up-with-pbs"&gt;What You Should Back Up With PBS
&lt;/h2&gt;&lt;p&gt;You probably don&amp;rsquo;t need to back up your entire lab with PBS. Back up the things that matter most.&lt;/p&gt;
&lt;p&gt;Back up with PBS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VMs that matter&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker stack VM&lt;/li&gt;
&lt;li&gt;Home Assistant or other important services&lt;/li&gt;
&lt;li&gt;Anything with a non-trivial config that would suck to rebuild&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LXC containers that matter&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reverse proxy&lt;/li&gt;
&lt;li&gt;DNS Servers&lt;/li&gt;
&lt;li&gt;Ansible and Playbooks&lt;/li&gt;
&lt;li&gt;Jellyfin / Plex&lt;/li&gt;
&lt;li&gt;Your media stack if you like LXCs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="things-that-pbs-is-not-ideal-for"&gt;Things that PBS is not ideal for
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Huge media libraries&lt;/strong&gt;
40 TB of movies do not belong in PBS. That is what your NAS redundancy is for. Worst case, you redownload.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cold archives for years&lt;/strong&gt;
You can keep long retention if you want to, but PBS is best for working backups of services, not glacier style storage.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
If it would take you hours to reconfigure by hand, put it in PBS. If you can redownload or recreate it easily, do not waste PBS space on it.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="a-backup-strategy-that-does-not-suck"&gt;A Backup Strategy That Does Not Suck
&lt;/h2&gt;&lt;p&gt;You need something simple enough that you will keep it.&lt;/p&gt;
&lt;p&gt;Here is what I run and it works well:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schedules&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nightly backups for critical VMs and LXCs (Immich, Home Assistant, reverse proxy, databases)&lt;/li&gt;
&lt;li&gt;Weekly backups for less critical stuff (test environments, secondary services)&lt;/li&gt;
&lt;li&gt;Backup window when nobody is streaming&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Retention&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep 4 backups total&lt;/li&gt;
&lt;li&gt;One monthly snapshot&lt;/li&gt;
&lt;li&gt;Everything older gets pruned automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives me short term protection against bad updates and my own stupid mistakes without eating up too much storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Turn on verification jobs, so PBS periodically checks backups for corruption. It is not enough to have files. You need files that restore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lightweight 3 2 1 (Future Goal)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you want to get fancy later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;3 copies of important data&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running VM or LXC&lt;/li&gt;
&lt;li&gt;PBS backup&lt;/li&gt;
&lt;li&gt;Optional offsite copy of PBS datastore or config&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2 types of media&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal PBS disk&lt;/li&gt;
&lt;li&gt;External USB drive or another storage box&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;1 offsite&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Could be a cloud bucket, a box at a friend&amp;rsquo;s house, whatever&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Do not overthink this from day one. Get PBS on a separate box first. Get nightly backups running. Then start thinking about offsite copies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="networking-and-security-for-pbs"&gt;Networking And Security For PBS
&lt;/h2&gt;&lt;p&gt;This box protects everything else. Don&amp;rsquo;t drop it on the same flat network as your kids&amp;rsquo; tablets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep it on a management VLAN&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Put PBS on a subnet used for servers and admin stuff&lt;/li&gt;
&lt;li&gt;Only Proxmox nodes and your admin machine should talk to it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Lock down firewall rules&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allow only Proxmox hosts and your admin box to connect on PBS ports&lt;/li&gt;
&lt;li&gt;Block general LAN clients from hitting the PBS web UI directly&lt;/li&gt;
&lt;li&gt;No random IoT gear or smart TVs talking to PBS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;No direct internet exposure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do not port forward PBS from your router&lt;/li&gt;
&lt;li&gt;If you need remote access, go through VPN
&lt;ul&gt;
&lt;li&gt;Tailscale, WireGuard, or a similar tunnel from your admin PC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;PBS doesn&amp;rsquo;t need to be a fortress, but it does need to be harder to reach and harder to destroy than the machines it is backing up.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="hardware-for-a-pbs-node"&gt;Hardware For A PBS Node
&lt;/h2&gt;&lt;p&gt;Good news. This box doesn&amp;rsquo;t need to be powerful. It only needs to be reliable and have enough disk.&lt;/p&gt;
&lt;p&gt;Here is what I am running:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My PBS Build (2013 DOGE Mining Rig)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The hardware that failed to make me rich mining meme cryptocurrency in 2013 is now the backbone of my backup strategy.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU: Intel G1610 (a decade old dual core)&lt;/li&gt;
&lt;li&gt;Motherboard: GIGABYTE GA-B75M-D3H&lt;/li&gt;
&lt;li&gt;RAM: 16GB&lt;/li&gt;
&lt;li&gt;System: 128GB SSD&lt;/li&gt;
&lt;li&gt;Storage: 2 Mirrored 4TB HDDs&lt;/li&gt;
&lt;li&gt;Case: 2U Rosewill&lt;/li&gt;
&lt;li&gt;Backing up: 8 VMs and 12 LXCs&lt;/li&gt;
&lt;li&gt;Current usage: 12% of 4TB after months of nightly/monthly backups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this ancient hardware can handle 20 guests with daily backups, you definitely do not need to overthink it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;General Guidelines&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 to 16 GB of RAM is plenty for most homelabs&lt;/li&gt;
&lt;li&gt;Small SSD for the PBS system (128GB works fine)&lt;/li&gt;
&lt;li&gt;Big HDD or SSD for datastore (2-4TB is a good start)&lt;/li&gt;
&lt;li&gt;CPU does not matter much, disk and network do&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You probably have spare hardware sitting around that is more than capable.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0B84STHJS"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/rosewill-2U_hu_a79b98fe19d6d092.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/rosewill-2U_hu_2dfbc94a186e93ed.webp" width="600" height="244" alt="Rosewill RSV-Z2800U 2U Rackmount Server Chassis" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Rosewill RSV-Z2800U&lt;/strong&gt;
This is the case I use for my PBS: It is a 2U rackmount server chassis that provides plenty of drive bays and solid airflow for a homelab or small server. It is a good pick if you want a sturdy case that can grow with your storage and hardware needs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3Y0jqbk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-happens-when-you-keep-ignoring-backups"&gt;What Happens When You Keep Ignoring Backups
&lt;/h2&gt;&lt;p&gt;Eventually you will break something.&lt;/p&gt;
&lt;p&gt;Maybe it&amp;rsquo;s an update that doesn&amp;rsquo;t play nice (looking at you, Jellyfin).
Maybe you type the wrong container ID in the terminal. A typo that destroys something important.&lt;/p&gt;
&lt;p&gt;Without PBS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stare at the error&lt;/li&gt;
&lt;li&gt;Dig through old blog posts to remember your setup&lt;/li&gt;
&lt;li&gt;Reinstall services one by one&lt;/li&gt;
&lt;li&gt;Fix permissions and rewire configs&lt;/li&gt;
&lt;li&gt;Hours disappear&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With PBS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open Proxmox&lt;/li&gt;
&lt;li&gt;Click restore&lt;/li&gt;
&lt;li&gt;Pick last night&amp;rsquo;s backup&lt;/li&gt;
&lt;li&gt;Wait five minutes&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Last month I upgraded Jellyfin from 10.10.7 to 10.11. The database migration failed. Pre-PBS that would have been a full day to rebuild and rescan my media. With PBS it was a five minute restore.&lt;/p&gt;
&lt;p&gt;Before PBS, I had anxiety about touching production services. I tiptoed around my own lab.&lt;/p&gt;
&lt;p&gt;Now I experiment freely. I test upgrades without fear. I try new configurations to see what breaks. Worst case, I lose 24 hours of data and spend five minutes on a restore.&lt;/p&gt;
&lt;h2 id="simple-setup-flow"&gt;Simple Setup Flow
&lt;/h2&gt;&lt;p&gt;Here is the high level checklist. You do not need a full tutorial to get started.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Grab a spare box or mini PC (seriously, check your closet)&lt;/li&gt;
&lt;li&gt;Install Proxmox Backup Server on it&lt;/li&gt;
&lt;li&gt;Add a datastore on a big disk&lt;/li&gt;
&lt;li&gt;On your Proxmox compute node, add PBS as a backup remote&lt;/li&gt;
&lt;li&gt;Create backup jobs for the VMs and LXCs that matter
&lt;ul&gt;
&lt;li&gt;Daily schedule for critical stuff&lt;/li&gt;
&lt;li&gt;Weekly for everything else&lt;/li&gt;
&lt;li&gt;Reasonable retention rules (start simple)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run the first backup and watch it complete&lt;/li&gt;
&lt;li&gt;Do a test restore into a new VM or LXC ID&lt;/li&gt;
&lt;li&gt;Confirm the restored guest boots and works&lt;/li&gt;
&lt;li&gt;Optionally: delete something on purpose to prove you can get it back&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After that, backups go from &amp;ldquo;I should do something&amp;rdquo; to &amp;ldquo;this is part of how the stack runs&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;p&gt;At this point you have four roles in your on prem cloud:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router at the edge&lt;/li&gt;
&lt;li&gt;NAS for storage&lt;/li&gt;
&lt;li&gt;Compute for apps and services&lt;/li&gt;
&lt;li&gt;Backup to save you from your own bad decisions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And this backup server? It is the one that lets you sleep at night.&lt;/p&gt;
&lt;h2 id="series-navigation"&gt;Series Navigation
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-hardware-guide-2025/" &gt;Part 1 - The 3 Boxes That Run Your Media Server (2025 Edition)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-router-2025" &gt;Part 2 - Why Your ISP Router Is Killing Your Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025" &gt;Part 3 - Your Storage Deserves Its Own Box&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-compute-2025" &gt;Part 4 - Proxmox Is Your New Best Friend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-backup-2025" &gt;Part 5 - Backups That Work (With PBS)&lt;/a&gt; ← You are here&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Best Proxmox Server Hardware: Homelab Compute Guide</title><link>https://diymediaserver.com/post/media-server-compute-2025/</link><pubDate>Sat, 22 Nov 2025 06:35:02 -0600</pubDate><guid>https://diymediaserver.com/post/media-server-compute-2025/</guid><description>&lt;img src="https://diymediaserver.com/post/media-server-compute-2025/featured_hu_75e6a980fe38df15.webp" alt="Featured image of post Best Proxmox Server Hardware: Homelab Compute Guide" /&gt;&lt;p&gt;It was a Tuesday.&lt;/p&gt;
&lt;p&gt;I was already at work when my phone rang. It was my wife: &amp;ldquo;Nothing is working this morning. No internet, no Jellyfin, nothing.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I walked her through checking the server. No lights. No fan noise. She tried the power button. Nothing.&lt;/p&gt;
&lt;p&gt;The PSU was dead. And with it? Everything. Router, storage, media server, home automation, my entire IT infrastructure in one box, dead.&lt;/p&gt;
&lt;p&gt;I left work, drove to BestBuy, bought a new PSU, and spent the next hour getting everything back online. Four hours where my wife couldn&amp;rsquo;t work from home, couldn&amp;rsquo;t stream anything, couldn&amp;rsquo;t do basic internet tasks. Outage total: about four hours.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when I committed to tearing it all apart.&lt;/p&gt;
&lt;p&gt;This is Part 4 in our series, and today we&amp;rsquo;re covering the dedicated compute node. The server that runs all your applications while your router handles traffic and your NAS stores your files.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running everything on one machine right now? This is your warning shot.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
A dedicated Proxmox compute server allows you to rebuild entire Docker stacks, snapshot VMs before risky changes, and reboot for updates. All without taking down your internet or storage. On the other hand, one box means one failure takes everything down. Three boxes means isolation, stability, and the freedom to break things safely.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-your-homelab-needs-a-dedicated-proxmox-compute-server"&gt;Why Your Homelab Needs a Dedicated Proxmox Compute Server
&lt;/h2&gt;&lt;p&gt;Running everything on one machine feels efficient. It feels smart. It is neither.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s complicated. And it&amp;rsquo;s fragile.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what actually happens when you cram routing, storage, and compute into a single Proxmox server:&lt;/p&gt;
&lt;h3 id="one-failure-takes-down-everything"&gt;One failure takes down everything.
&lt;/h3&gt;&lt;p&gt;One dead PSU. One bad kernel update. One stuck VM. Suddenly your internet is gone, your files are inaccessible, and your entire household is asking when things will work again.&lt;/p&gt;
&lt;h3 id="reboots-become-a-crisis"&gt;Reboots become a crisis.
&lt;/h3&gt;&lt;p&gt;Need to apply Proxmox updates? Cool. Your router goes down. Your NAS goes offline. Every service you run disappears for ten minutes while you pray everything comes back in the right order.&lt;/p&gt;
&lt;h3 id="boot-order-becomes-a-nightmare"&gt;Boot order becomes a nightmare.
&lt;/h3&gt;&lt;p&gt;I ran a virtualized NAS on my all-in-one Proxmox server for way too long. Proxmox wouldn&amp;rsquo;t reliably mount NFS shares before LXCs like Jellyfin tried to start. I tried systemd automounts, autofs, custom scripts, all of it. Nothing was reliable enough. Every boot was a gamble. Would Jellyfin find its media? Would the shares even mount? I&amp;rsquo;d spend ten minutes hand-holding the server back to life, restarting services manually until everything lined up.&lt;/p&gt;
&lt;h3 id="one-workload-can-kill-everything-else"&gt;One workload can kill everything else.
&lt;/h3&gt;&lt;p&gt;Kick off a Tdarr batch transcode job and watch your CPU pin at 100%. Now your router is struggling. DNS queries are timing out. Someone&amp;rsquo;s work VPN drops mid-call. All because you decided to transcode some files.&lt;/p&gt;
&lt;h3 id="a-full-filesystem-cascades"&gt;A full filesystem cascades.
&lt;/h3&gt;&lt;p&gt;One container fills your root partition with logs. Now your storage is broken too. Your apps won&amp;rsquo;t start. Your VMs won&amp;rsquo;t boot. And you&amp;rsquo;re stuck SSHing in to clean up the mess before anything works again.&lt;/p&gt;
&lt;h2 id="what-separation-gives-you"&gt;What Separation Gives You
&lt;/h2&gt;&lt;p&gt;Splitting compute away from routing and storage fixes all of this before it becomes a crisis.&lt;/p&gt;
&lt;h3 id="fault-isolation-that-holds"&gt;Fault isolation that holds.
&lt;/h3&gt;&lt;p&gt;A few weeks ago I completely rebuilt several LXCs and VMs on my compute box. I moved the entire Arr suite from separate LXC containers into one Docker VM. I rebuilt my Immich server from scratch. Nobody noticed. Not a single complaint. The only time my wife knew something was happening was when I asked her to let me know when her show was over so I could reboot the server. Four minutes later, Jellyfin was back and she kept watching.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what separation gives you. The freedom to break things, test things, rebuild things, all without taking down the internet or losing access to your files.&lt;/p&gt;
&lt;h3 id="sane-boot-order"&gt;Sane boot order.
&lt;/h3&gt;&lt;p&gt;Your NAS boots after the router. NFS and SMB shares are ready and waiting. Your router is already routing. Then your Proxmox compute node boots, mounts storage paths, and starts services in a predictable order. No more race conditions. No more &amp;ldquo;will it work this time&amp;rdquo; anxiety. Things come up clean.&lt;/p&gt;
&lt;h3 id="better-performance-across-the-board"&gt;Better performance across the board.
&lt;/h3&gt;&lt;p&gt;Transcoding, Sonarr indexing, torrent hashing, container updates, all of that lives on hardware designed to handle it. Your NAS focuses on serving files fast. Your router focuses on routing packets and running OPNsense. Everyone stays in their lane and does their job well.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This isn&amp;rsquo;t overkill.&lt;br&gt;
This is building your homelab correctly. With purpose. So it works reliably every single time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5)&lt;/strong&gt;
Compact mini PC for lightweight Proxmox servers capable of GPU Passthrough, Several VMs, and LXCs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-the-proxmox-compute-node-does"&gt;What the Proxmox Compute Node Does
&lt;/h2&gt;&lt;p&gt;The compute node is where your Homelab apps and services live.&lt;/p&gt;
&lt;p&gt;This is the server where you run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Media servers:&lt;/strong&gt; Jellyfin, Plex, Emby&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media automation:&lt;/strong&gt; Sonarr, Radarr, Prowlarr, Bazarr, Lidarr&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download clients:&lt;/strong&gt; qBittorrent, SABnzbd, NZBGet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media processing:&lt;/strong&gt; Tdarr for transcoding and file cleanup&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reverse proxy:&lt;/strong&gt; Nginx Proxy Manager or Traefik&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote access:&lt;/strong&gt; Tailscale, Cloudflare Tunnel, or WireGuard&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-hosted services:&lt;/strong&gt; Home Assistant, Immich, Audiobookshelf, Kavita, Nextcloud&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Proxmox compute server is responsible for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hosting all your containers, VMs, and LXCs&lt;/li&gt;
&lt;li&gt;Handling heavy CPU loads like video transcoding, media analysis, and file processing&lt;/li&gt;
&lt;li&gt;Mounting storage from your NAS over NFS or SMB without breaking when mounts are slow&lt;/li&gt;
&lt;li&gt;Exposing services safely through your router and reverse proxy&lt;/li&gt;
&lt;li&gt;Keeping everything isolated enough that when one app crashes, it doesn&amp;rsquo;t cascade&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Think of it as the app layer in your home data center architecture. Router at the edge. NAS storage at the bottom. Compute in the middle doing the heavy lifting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-proxmox-ve-is-the-right-hypervisor-for-homelab-compute"&gt;Why Proxmox VE Is the Right Hypervisor for Homelab Compute
&lt;/h2&gt;&lt;p&gt;You can absolutely run everything on bare metal Linux with Docker. You can force it.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll also hate yourself when you want to move a service, pass through a GPU, snapshot before an upgrade, or deal with backups that don&amp;rsquo;t suck.&lt;/p&gt;
&lt;p&gt;Proxmox VE solves most of those problems with one installation.&lt;/p&gt;
&lt;h3 id="why-proxmox-works-so-well-for-homelab-compute-servers"&gt;Why Proxmox works so well for homelab compute servers:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Web UI that doesn&amp;rsquo;t make you want to throw your keyboard&lt;/li&gt;
&lt;li&gt;Native support for both LXC containers and full VMs&lt;/li&gt;
&lt;li&gt;Easy snapshots and backups that actually save you when things break&lt;/li&gt;
&lt;li&gt;Good integration with NFS, SMB, and iSCSI for mounting external NAS storage&lt;/li&gt;
&lt;li&gt;Straightforward PCIe and GPU passthrough for hardware transcoding&lt;/li&gt;
&lt;li&gt;Flexible enough to grow into Proxmox clusters later if you add more nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="lxc-vs-vms-vs-docker-in-proxmox"&gt;LXC vs VMs vs Docker in Proxmox
&lt;/h3&gt;&lt;p&gt;You have three layers to work with in Proxmox VE:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Best Fit&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lightweight Linux services&lt;/td&gt;
&lt;td&gt;LXC&lt;/td&gt;
&lt;td&gt;Low overhead, fast boot, easy resource limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full OS instances&lt;/td&gt;
&lt;td&gt;VM&lt;/td&gt;
&lt;td&gt;When you need complete isolation or non-Linux guests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App-level deployment&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Huge ecosystem, easy configs, stacks and compose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Here&amp;rsquo;s a sane Proxmox layout pattern that actually works in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox VE at the base layer&lt;/li&gt;
&lt;li&gt;A few LXC containers for core infrastructure services (reverse proxy, monitoring)&lt;/li&gt;
&lt;li&gt;One or more VMs running Docker for your application stacks&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-use-vms-for-docker-instead-of-lxcs"&gt;Why use VMs for Docker instead of LXCs?
&lt;/h3&gt;&lt;p&gt;Proxmox themselves advise against running Docker inside LXC containers. Running containers inside containers gets weird fast. You&amp;rsquo;ll hit issues with nested namespaces, cgroup limitations, and storage drivers. A VM gives you clean kernel isolation and full control without the headaches.&lt;/p&gt;
&lt;h4 id="my-current-proxmox-compute-server-setup-as-an-example"&gt;My current Proxmox compute server setup as an example:
&lt;/h4&gt;&lt;p&gt;I&amp;rsquo;m running an Intel i5-12600K with 64 GB of RAM, a 6-disk RAID-Z2 array for protected VM and LXC storage, and a 2 TB NVMe drive for Proxmox itself plus workloads that need fast, unprotected storage.&lt;/p&gt;
&lt;p&gt;On top of that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jellyfin&lt;/strong&gt; - Runs in an LXC so I can pass through Intel QuickSync for hardware transcoding without losing the ability to share the iGPU with other containers or the Proxmox host&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker VM for the Arr suite&lt;/strong&gt; - Sonarr, Radarr, Prowlarr, all in one VM managed with Docker Compose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker VM for automation tools&lt;/strong&gt; - n8n, Node-RED, OpenWebUI, and Home Assistant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager LXC&lt;/strong&gt; - Reverse proxy handling all external access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Individual service LXCs&lt;/strong&gt; - Audiobookshelf, Ansible, Hugo - Each doing one job, cleanly isolated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don&amp;rsquo;t share media outside my house because cable upload speeds are trash, but inside the house I&amp;rsquo;ve never hit issues with up to three simultaneous 4K streams.&lt;/p&gt;
&lt;p&gt;And now? I can rebuild or test elements of it without anyone noticing. This is the point of separating compute from the router and storage.&lt;/p&gt;
&lt;h2 id="storage-access-in-proxmox-without-the-pain"&gt;Storage Access in Proxmox Without the Pain
&lt;/h2&gt;&lt;p&gt;This is where a lot of homelab builders get burned.&lt;/p&gt;
&lt;p&gt;Your Proxmox compute node should not be where your media lives. It should be where your media is accessed and used.&lt;/p&gt;
&lt;h3 id="heres-the-right-way-to-handle-storage"&gt;Here&amp;rsquo;s the right way to handle storage:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Media and files live on your dedicated NAS server&lt;/li&gt;
&lt;li&gt;NAS exports storage via NFS or SMB shares&lt;/li&gt;
&lt;li&gt;Proxmox compute box mounts those shares and passes them into LXCs and VMs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My NAS is bare metal Debian 13 with a MergerFS pool, connected to the compute box over 10 GbE. It serves both NFS and SMB shares depending on what needs them.&lt;/p&gt;
&lt;h3 id="the-storage-migration-process"&gt;The Storage Migration Process
&lt;/h3&gt;&lt;p&gt;When I finally pulled storage off the all-in-one, the process was straightforward but took some planning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Build the new NAS first.&lt;/strong&gt;
I installed Debian 13, MergerFS, NFS, and SMB.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Physically move the Hard Drives and HBA.&lt;/strong&gt;
Pulled the HBA and 7 drives out of the all-in-one host and moved them to the new dedicated hardware.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Configure MergerFS, NFS, and SMB.&lt;/strong&gt;
Configured the MergerFS storage pool. Configured NFS and SMB shares.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Verify everything.&lt;/strong&gt;
I left the old storage VM off but in place for a week. In case anything went sideways. Once I was confident, I removed it and I haven&amp;rsquo;t looked back since.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="rules-to-save-you-from-storage-headaches"&gt;Rules to Save You From Storage Headaches
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Use consistent mount points across everything.&lt;/strong&gt;&lt;br&gt;
Pick a path structure and stick to it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/media/storage/movies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media/storage/shows&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media/storage/music&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mount these into your Proxmox LXCs and VMs so the paths inside match exactly. Life is dramatically easier when Jellyfin, the Arr suite, and SABnzbd all agree on where &lt;code&gt;/movies&lt;/code&gt;, &lt;code&gt;/shows&lt;/code&gt;, and &lt;code&gt;/music&lt;/code&gt; live.&lt;/p&gt;
&lt;h3 id="keep-transcode-and-temp-work-on-local-ssd"&gt;Keep transcode and temp work on local SSD.
&lt;/h3&gt;&lt;p&gt;Let Jellyfin and Tdarr use local NVMe or SSD paths for transcoding cache and analysis. Don&amp;rsquo;t hammer your NAS with endless small writes if you don&amp;rsquo;t have to. Your NAS will thank you, and your transcodes will be faster.&lt;/p&gt;
&lt;h3 id="mount-read-only-where-you-can"&gt;Mount read-only where you can.
&lt;/h3&gt;&lt;p&gt;For plain media directories that rarely change, mount them read-only to services that only need to read. A misbehaving app can&amp;rsquo;t delete your entire movie library if it doesn&amp;rsquo;t have write access.&lt;/p&gt;
&lt;h3 id="network-configuration-for-proxmox-and-nas"&gt;Network Configuration for Proxmox and NAS
&lt;/h3&gt;&lt;p&gt;Your Proxmox compute node and NAS should be on the same network for best performance. Use static IP addresses for both so your mounts don&amp;rsquo;t break if DHCP leases change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For 1 GbE networks:&lt;/strong&gt; This works fine for most home users. You&amp;rsquo;ll get roughly 100-115 MB/s transfer speeds, which handles multiple 1080p streams and light 4K transcoding without issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For 2.5 GbE networks:&lt;/strong&gt; The sweet spot for cost vs performance in 2025. You&amp;rsquo;ll get 250-280 MB/s, plenty for heavy 4K streaming and multiple simultaneous transcodes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For 10 GbE networks:&lt;/strong&gt; Overkill for most, but if you&amp;rsquo;re running lots of VMs, doing heavy transcoding, or have multiple users, the 1000+ MB/s speeds make everything feel instant.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07H289S7C"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate-8tb_hu_3ac46966354d0ce6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate-8tb_hu_892fe1aa3bbbb134.webp" width="600" height="861" alt="Seagate BarraCuda Internal Hard Drive 8TB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate BarraCuda Internal Hard Drive 8TB&lt;/strong&gt;&lt;br&gt;
Right now one of the best price per GB you can find. Also has a 2-year warranty.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/45Cw97V" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="best-proxmox-compute-server-hardware-for-2025"&gt;Best Proxmox Compute Server Hardware for 2025
&lt;/h2&gt;&lt;p&gt;You don&amp;rsquo;t need a screaming 1U datacenter monster for a good Proxmox homelab. You need the right balance of CPU cores, RAM, and storage.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what that looks like.&lt;/p&gt;
&lt;h3 id="budget-proxmox-compute-server-build"&gt;Budget Proxmox Compute Server Build
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Specs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4-core CPU with VT-x/AMD-V support&lt;/li&gt;
&lt;li&gt;8-16 GB RAM (minimum 8 GB)&lt;/li&gt;
&lt;li&gt;250-500 GB SSD for Proxmox and VMs&lt;/li&gt;
&lt;li&gt;1 GbE or 2.5 GbE NIC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1080p media streaming with Jellyfin or Plex&lt;/li&gt;
&lt;li&gt;2-3 simultaneous users&lt;/li&gt;
&lt;li&gt;A handful of Docker containers in one VM&lt;/li&gt;
&lt;li&gt;Light home automation and a few LXCs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="why-16gb-of-ram"&gt;Why 16GB of RAM?
&lt;/h4&gt;&lt;p&gt;Proxmox itself needs about 2 GB minimum. Each VM or heavy LXC will want 2-4 GB depending on workload. With 8 GB total, you can run Proxmox plus 2-3 lightweight VMs or several LXCs comfortably. Bump to 16 GB if you want headroom.&lt;/p&gt;
&lt;h4 id="hardware-examples"&gt;Hardware examples:
&lt;/h4&gt;&lt;p&gt;Used Dell OptiPlex 7050 SFF, HP EliteDesk 800 G3, Lenovo ThinkCentre Tiny M720q, or any decent mini PC with an Intel chip that has Quick Sync. Add an SSD, maybe bump the RAM if it&amp;rsquo;s cheap, install Proxmox VE, and you&amp;rsquo;re running.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This tier works if your needs are simple and you&amp;rsquo;re getting started with Proxmox homelabs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/dell_wyse_5070_hu_48b8240817f351c5.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/dell_wyse_5070_hu_d039890e1b74ebb8.webp" width="600" height="632" alt="Dell Wyse 5070 Extended (J5005)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Dell Wyse 5070 Extended (J5005)&lt;/strong&gt;&lt;br&gt;
This is the budget-friendly, low-power Proxmox box that works out of the box. Add an Intel i350-T2, give it 8–16 GB of RAM, and you&amp;rsquo;ve got a quiet, reliable home router with room for several LXCs and VMs.&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: $80 - $170
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=Dell&amp;#43;Wyse&amp;#43;5070&amp;#43;Extended&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="balanced-proxmox-compute-server-build-recommended"&gt;Balanced Proxmox Compute Server Build (Recommended)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Specs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;6-8 cores (modern Intel i5/i7)&lt;/li&gt;
&lt;li&gt;16-32 GB RAM (32 GB strongly recommended)&lt;/li&gt;
&lt;li&gt;500 GB - 1 TB NVMe SSD for fast VM storage&lt;/li&gt;
&lt;li&gt;Intel iGPU (for Quick Sync hardware transcoding)&lt;/li&gt;
&lt;li&gt;2.5 GbE NIC preferred&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1080p and 4K transcoding with hardware acceleration&lt;/li&gt;
&lt;li&gt;Multiple simultaneous users and media streams&lt;/li&gt;
&lt;li&gt;Running several VMs alongside multiple LXCs&lt;/li&gt;
&lt;li&gt;Full Arr stack in Docker plus Jellyfin, Home Assistant, and more&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="why-this-is-the-sweet-spot-for-most-proxmox-homelabs"&gt;Why this is the sweet spot for most Proxmox homelabs:
&lt;/h4&gt;&lt;p&gt;This is where I landed. My i5-12600K build with 64 GB of RAM and local RAID-Z2 storage for VM protection handles everything I throw at it. Jellyfin transcodes 4K HEVC smoothly with QuickSync. The Arr suite runs in Docker without breaking a sweat. I can snapshot VMs, rebuild entire stacks, test new configs and nobody in the house notices unless I tell them.&lt;/p&gt;
&lt;h4 id="why-32-gb-ram-instead-of-16-gb"&gt;Why 32 GB RAM instead of 16 GB?
&lt;/h4&gt;&lt;p&gt;Here&amp;rsquo;s the reality: Proxmox itself uses 2 GB. Each Docker VM will want 4-8 GB depending on how many containers you&amp;rsquo;re running. Jellyfin in an LXC will use 2-4 GB during transcodes. If you&amp;rsquo;re running ZFS on your local storage (like my RAID-Z2), ZFS will want to use up to 50% of available RAM for its ARC cache.&lt;/p&gt;
&lt;p&gt;With 16 GB total, you&amp;rsquo;ll constantly be swap-constrained and performance will suffer. With 32 GB, you have room to grow and VMs aren&amp;rsquo;t fighting each other for memory. With 64 GB like I have, you can run almost anything without thinking twice.&lt;/p&gt;
&lt;h4 id="cpu-considerations"&gt;CPU considerations:
&lt;/h4&gt;&lt;p&gt;Modern Intel CPUs (12th gen and newer) are ideal. Intel has an edge here if you care about hardware transcoding because Quick Sync support in Jellyfin and Plex is excellent and dramatically reduces CPU load.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running lots of containers or VMs, prioritize core count. Each VM you run will want 2-4 dedicated cores for good performance. Try not to over-provision. If you assign more vCPUs to VMs than you have physical cores, Proxmox will time-share and everything slows down.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This tier is perfect if you&amp;rsquo;re serious about a Proxmox homelab media setup that works reliably.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="product-box" data-asin="B0D454DQSP"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;&lt;br&gt;
The MS-01 i5 is a tiny mini PC with plenty of cores, multiple NVMe slots, and real homelab networking (dual 10G SFP+ plus 2.5 GbE), which makes it perfect for a Proxmox compute node. It has enough power for Jellyfin, the *arr stack, downloads, and a few VMs or LXCs, without being a space heater.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4p3HhTI" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="high-end-proxmox-compute-server-build"&gt;High-End Proxmox Compute Server Build
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Specs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8+ cores with high clock speeds (Intel i7/i9)&lt;/li&gt;
&lt;li&gt;64-128 GB RAM&lt;/li&gt;
&lt;li&gt;Large NVMe (1-2 TB) for VMs and fast workloads&lt;/li&gt;
&lt;li&gt;Dedicated GPU for transcoding or compute tasks (optional)&lt;/li&gt;
&lt;li&gt;10 GbE NIC if your NAS can keep up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Heavy 4K HEVC transcoding workloads&lt;/li&gt;
&lt;li&gt;Multiple simultaneous remote transcode streams&lt;/li&gt;
&lt;li&gt;Running many VMs and test environments in Proxmox clusters&lt;/li&gt;
&lt;li&gt;Extra workloads like game servers, AI/LLM tools, or development environments&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="when-this-tier-makes-sense"&gt;When this tier makes sense:
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;re running significantly more than media services, if you&amp;rsquo;re sharing your Proxmox setup with people outside your house, or if you&amp;rsquo;re experimenting heavily with nested virtualization and want room to grow without limits.&lt;/p&gt;
&lt;p&gt;For a simple household Proxmox homelab? This is overkill. But if you have the use cases and the power budget, it&amp;rsquo;s very nice to have all that headroom.&lt;/p&gt;
&lt;h2 id="recommended-proxmox-layout-for-your-compute-node"&gt;Recommended Proxmox Layout for Your Compute Node
&lt;/h2&gt;&lt;p&gt;You don&amp;rsquo;t need anything complicated to start with Proxmox VE.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a clean layout that works for most homelab setups:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure LXC:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nginx Proxy Manager or Traefik for reverse proxy&lt;/li&gt;
&lt;li&gt;Tailscale, WireGuard, or Cloudflare Tunnel for secure remote access&lt;/li&gt;
&lt;li&gt;This container stays up when everything else is being rebuilt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Primary Docker VM:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu or Debian VM running Docker and Docker Compose&lt;/li&gt;
&lt;li&gt;Your entire media stack: Arr suite, download clients, media automation&lt;/li&gt;
&lt;li&gt;Easy to snapshot before major changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Optional Secondary Docker VM:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Separate VM for experimental or unrelated Docker workloads&lt;/li&gt;
&lt;li&gt;Automation tools like n8n, Home Assistant if you want it containerized&lt;/li&gt;
&lt;li&gt;Keeps your core media stack isolated from other projects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Individual Service LXCs:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jellyfin or Plex (LXC for GPU passthrough flexibility)&lt;/li&gt;
&lt;li&gt;Audiobookshelf, Immich, or other single-purpose services&lt;/li&gt;
&lt;li&gt;Utility containers for scripts, monitoring, or tools you&amp;rsquo;re testing&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Group related services together in VMs, but don&amp;rsquo;t cram everything into one container. Keep it modular enough that you can blow up your Docker VM and rebuild it from scratch without taking out your reverse proxy, remote access, or other critical infrastructure.
This gives you enough isolation to experiment safely in Proxmox without overcomplicating your setup.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="real-world-benefits-of-a-dedicated-proxmox-compute-server"&gt;Real World Benefits of a Dedicated Proxmox Compute Server
&lt;/h2&gt;&lt;p&gt;Once you split compute away from routing and storage, a lot of things stop being crises.&lt;/p&gt;
&lt;h3 id="you-can-reboot-proxmox-without-taking-down-the-house"&gt;You can reboot Proxmox without taking down the house.
&lt;/h3&gt;&lt;p&gt;Need to apply Proxmox kernel updates? Need to test a hardware change? Go ahead. Your router stays up. Your NAS stays accessible. Jellyfin goes down for a few minutes and comes back. No one freaks out.&lt;/p&gt;
&lt;h3 id="you-can-rebuild-services-without-fear"&gt;You can rebuild services without fear.
&lt;/h3&gt;&lt;p&gt;Snapshot your Docker VM before making changes. Try a new Jellyfin major version. Completely rebuild your Arr stack with different Docker Compose configs. If it breaks, roll back the Proxmox snapshot in 30 seconds. If it works, keep it and delete the old snapshot. Your NAS and router don&amp;rsquo;t care either way.&lt;/p&gt;
&lt;h3 id="you-can-stress-test-during-off-hours-without-affecting-critical-services"&gt;You can stress test during off hours without affecting critical services.
&lt;/h3&gt;&lt;p&gt;Want to run Tdarr overnight and transcode your entire 4K library? Go for it. Let the CPU pin at 100% all night. Your router isn&amp;rsquo;t competing for cycles. Your NAS isn&amp;rsquo;t getting hammered by transcode temp files.&lt;/p&gt;
&lt;h3 id="your-family-notices-the-difference-in-a-good-way"&gt;Your family notices the difference, in a good way.
&lt;/h3&gt;&lt;p&gt;I used to hear &amp;ldquo;the internet is down AGAIN&amp;rdquo; all the time. Now? Things work. My wife doesn&amp;rsquo;t think about the server anymore. It&amp;rsquo;s invisible. That&amp;rsquo;s exactly the goal.&lt;/p&gt;
&lt;p&gt;And since splitting things up, I&amp;rsquo;ve even been able to share my Jellyfin server with one friend outside my house. Not because my upload speeds got better (they&amp;rsquo;re still trash) but because the system is stable enough that I trust it to work consistently without me babysitting it.&lt;/p&gt;
&lt;h2 id="the-real-risks-of-running-everything-on-one-proxmox-box"&gt;The Real Risks of Running Everything on One Proxmox Box
&lt;/h2&gt;&lt;p&gt;If you keep routing, storage, and compute all on one Proxmox server, here&amp;rsquo;s what you&amp;rsquo;re signing up for:&lt;/p&gt;
&lt;h3 id="hardware-failures-cascade-completely"&gt;Hardware failures cascade completely.
&lt;/h3&gt;&lt;p&gt;PSU dies like mine did? Motherboard failure? Bad RAM stick? Your entire digital life disappears. Internet, storage, every app, every VM. All gone until you fix or replace hardware.&lt;/p&gt;
&lt;h3 id="software-issues-spread-everywhere"&gt;Software issues spread everywhere.
&lt;/h3&gt;&lt;p&gt;Kernel panic during a Proxmox upgrade? One VM consuming all available RAM? A filled root partition from runaway Docker logs? Now routing is broken, storage is inaccessible, and all your apps are down.&lt;/p&gt;
&lt;h3 id="maintenance-becomes-high-risk"&gt;Maintenance becomes high-risk.
&lt;/h3&gt;&lt;p&gt;Want to add more RAM? Swap a drive? Update BIOS? Better hope it goes smoothly, because if something goes wrong during maintenance, everything is offline until you recover.&lt;/p&gt;
&lt;h3 id="boot-order-is-a-constant-gamble"&gt;Boot order is a constant gamble.
&lt;/h3&gt;&lt;p&gt;Will the NAS VM start before other services try to mount shares? Will everything come up in the right sequence? Or will you spend 15 minutes manually restarting services until things work?&lt;/p&gt;
&lt;h3 id="no-room-for-safe-experimentation"&gt;No room for safe experimentation.
&lt;/h3&gt;&lt;p&gt;Want to test a major Proxmox upgrade? Try a different hypervisor kernel? Experiment with GPU passthrough? Better be confident, because if it breaks, you&amp;rsquo;re taking down the router and storage too.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This isn&amp;rsquo;t &amp;ldquo;maybe someday.&amp;rdquo; I lived it. The PSU failure was my wake-up call, but the constant virtualized NAS mount issues and boot order anxiety were already wearing me down.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="how-to-start-your-roadmap-to-separating-compute"&gt;How to Start: Your Roadmap to Separating Compute
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re running everything on one box right now and this all sounds overwhelming, here&amp;rsquo;s the actual migration path:&lt;/p&gt;
&lt;h3 id="step-1-plan-and-budget-for-dedicated-nas-storage-first"&gt;Step 1: Plan and budget for dedicated NAS storage first.
&lt;/h3&gt;&lt;p&gt;That&amp;rsquo;s what I moved first, and it was absolutely the right call. Storage was causing me the most pain with the virtualized NAS and mount issues. Getting that onto dedicated hardware immediately made everything more stable.&lt;/p&gt;
&lt;p&gt;Figure out your NAS platform (TrueNAS, Unraid, or bare metal Linux with ZFS/MergerFS). Budget for drives, an HBA if you need one, and a box to house it all. You don&amp;rsquo;t need to buy enterprise-grade equipment, consumer hardware works fine for homelab use.&lt;/p&gt;
&lt;h3 id="step-2-build-the-nas-and-migrate-storage"&gt;Step 2: Build the NAS and migrate storage.
&lt;/h3&gt;&lt;p&gt;Set up your NAS with ZFS or your chosen filesystem. Configure NFS and SMB shares. Test everything thoroughly. Then move your drives over.&lt;/p&gt;
&lt;p&gt;Keep your current NAS VM off but available to restore if you need to. Once everything is migrated and verified, you can delete your old VM (I kept mine for a week or so).&lt;/p&gt;
&lt;h3 id="step-3-move-your-router-when-budget-allows"&gt;Step 3: Move your router when budget allows.
&lt;/h3&gt;&lt;p&gt;This takes a bit more planning and usually some dedicated hardware budget. But once storage is separate, your next priority is getting routing off the all-in-one. A dedicated pfSense or OPNsense box, or even a good prosumer router if you don&amp;rsquo;t need advanced firewall features.&lt;/p&gt;
&lt;h3 id="step-4-whats-left-is-your-dedicated-compute-node"&gt;Step 4: What&amp;rsquo;s left is your dedicated compute node.
&lt;/h3&gt;&lt;p&gt;Once routing and storage are separated, what remains is your Proxmox compute server. You might want to add more RAM or upgrade storage for better VM performance, but the foundation is already there. Keep running VMs and containers on dedicated compute hardware.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You don&amp;rsquo;t have to do this overnight. Each step makes the next one safer. Each separation reduces the blast radius when something goes wrong.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="what-i-wish-i-knew-before-starting"&gt;What I Wish I Knew Before Starting
&lt;/h2&gt;&lt;p&gt;If I could go back and tell myself one thing before beginning this Proxmox homelab journey, it would be this:&lt;/p&gt;
&lt;h3 id="learn-how-proxmox-handles-storage-pools-and-zfs-arrays-before-you-provision-anything"&gt;Learn how Proxmox handles storage pools and ZFS arrays before you provision anything.
&lt;/h3&gt;&lt;p&gt;I wasted hours trying to figure out Proxmox storage configuration, mount points, and how Proxmox integrates with external NFS shares because I didn&amp;rsquo;t understand the basics up front. Thirty minutes of reading the official Proxmox documentation would have saved me multiple evenings of trial and error.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re building local RAID-Z2 or hardware RAID storage in your compute box, understand how Proxmox will use it before you create storage pools and start deploying VMs.&lt;/p&gt;
&lt;h3 id="dont-cheap-out-on-cpu-and-ram-for-your-intended-workload"&gt;Don&amp;rsquo;t cheap out on CPU and RAM for your intended workload.
&lt;/h3&gt;&lt;p&gt;Buy hardware appropriate for the workloads you plan to run. If you know you&amp;rsquo;ll be transcoding 4K, running multiple Docker stacks, and hosting several VMs, don&amp;rsquo;t try to save $100 by buying inadequate hardware. You&amp;rsquo;ll end up buying better hardware later anyway, and you&amp;rsquo;ll have wasted time being frustrated by poor performance.&lt;/p&gt;
&lt;p&gt;Under-provisioning RAM is especially painful. If you don&amp;rsquo;t have enough RAM, Proxmox will swap to disk constantly and everything slows to a crawl. Same thing happens if you over-provision CPU cores across too many VMs. Time-sharing kills performance.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build it right the first time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="frequently-asked-questions-about-proxmox-compute-servers"&gt;Frequently Asked Questions About Proxmox Compute Servers
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How much RAM do I need for a Proxmox compute server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Minimum 8 GB for basic setups, but 16-32 GB is recommended for most homelab use cases. Proxmox itself needs about 2 GB. Each VM wants 2-8 GB depending on workload. If you&amp;rsquo;re using ZFS for local VM storage, ZFS will consume up to 50% of available RAM for caching. With 32 GB total, you have comfortable headroom for several VMs, LXCs, and ZFS caching without hitting swap.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I run Docker directly in Proxmox LXC containers?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Technically yes, but Proxmox and the community advise against it. Running Docker inside LXC containers creates nested containerization which leads to issues with namespaces, cgroups, and storage drivers. The recommended approach is running Docker inside a dedicated VM (Ubuntu or Debian), which gives you full kernel control and avoids compatibility headaches.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s the difference between LXC and VMs in Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;LXC containers share the host kernel and are extremely lightweight. They boot in seconds and use minimal overhead. VMs run their own complete operating system with full isolation. Use LXCs for Linux services where you want efficiency (like Nginx Proxy Manager). Use VMs when you need complete isolation, want to run Windows, or need full kernel control for things like Docker.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need a dedicated GPU for Jellyfin transcoding in Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not necessarily. Modern Intel CPUs (8th gen and newer) have excellent Quick Sync support built into the iGPU, which handles hardware transcoding very well. You can pass the iGPU through to a Jellyfin LXC or VM for hardware acceleration. A dedicated GPU only makes sense if you&amp;rsquo;re transcoding many simultaneous 4K streams or running other GPU workloads like AI inference.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use consumer SSDs for Proxmox VM storage?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, for homelab using consumer NVMe or SSDs is fine. Enterprise drives with Power-Loss Protection are better for production environments, but for home servers where the stakes are lower, good consumer SSDs like Samsung 970/980 EVO or Crucial P3/P5 will serve you well. Make sure you have backups. Consumer SSDs can fail more often, and when they do, they fail catastrophically.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I run my router as a VM in Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For homelab experimentation, yes. For production use where your household depends on internet access, no. If your Proxmox host crashes or needs maintenance, a virtualized router means your entire house loses internet. A dedicated router or firewall appliance running pfSense/OPNsense gives you the isolation you need so compute and routing failures don&amp;rsquo;t cascade.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What&amp;#39;s better for Proxmox storage: hardware RAID or ZFS?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For VM storage on the Proxmox compute node, ZFS is generally preferred. ZFS gives you built-in snapshots, compression, and data integrity checking. Hardware RAID can be faster in some cases but lacks ZFS features. Important: ZFS and hardware RAID controllers don&amp;rsquo;t mix. If you want ZFS, you need direct disk access (HBA mode or no RAID controller). For NAS storage, either approach works depending on your preference.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="whats-next-in-the-proxmox-homelab-series"&gt;What&amp;rsquo;s Next in the Proxmox Homelab Series
&lt;/h2&gt;&lt;p&gt;You have the router running pfSense or OPNsense.
You have the NAS providing reliable storage.
Now you have the dedicated Proxmox compute server running your applications.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s one more critical piece to add: &lt;strong&gt;the Proxmox Backup Server.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Next up is Part 5, where we cover building a Proxmox Backup Server that can actually save you when things go sideways. Because they will. And when your compute node crashes or you accidentally delete the wrong VM, you&amp;rsquo;ll be very glad you have working backups.&lt;/p&gt;
&lt;p&gt;Until then: separate your compute. Your future self will thank you.&lt;/p&gt;
&lt;h2 id="series-navigation"&gt;Series Navigation
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post//media-server-hardware-guide-2025" &gt;Part 1 - The 3 Boxes That Run Your Media Server (2025 Edition)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-router-2025" &gt;Part 2 - Why Your ISP Router Is Killing Your Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025" &gt;Part 3 - Your Storage Deserves Its Own Box&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-compute-2025" &gt;Part 4 - Proxmox Is Your New Best Friend&lt;/a&gt; ← You are here&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-backup-2025" &gt;Part 5 - Backups That Work (With PBS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Best NAS for Media Servers (2026): Dedicated Storage Guide</title><link>https://diymediaserver.com/post/media-server-storage-2025/</link><pubDate>Mon, 10 Nov 2025 07:30:02 -0600</pubDate><guid>https://diymediaserver.com/post/media-server-storage-2025/</guid><description>&lt;img src="https://diymediaserver.com/post/media-server-storage-2025/featured_hu_5d22cebb3ce6d5d6.webp" alt="Featured image of post Best NAS for Media Servers (2026): Dedicated Storage Guide" /&gt;&lt;p&gt;Power came back after an outage. My Proxmox host boots up. The VM that runs my NAS starts&amp;hellip; slowly. My Jellyfin container mounts &lt;code&gt;/mnt/media&lt;/code&gt; before the NFS share is ready. The mount succeeds but points to an empty directory. My library: &amp;ldquo;No items found.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I fixed it in 10 minutes. But I&amp;rsquo;d already done this dance multiple times a week for eight months. I hated having to manually mount the drives every time the server boots.&lt;/p&gt;
&lt;p&gt;The problem wasn&amp;rsquo;t the hardware. It wasn&amp;rsquo;t the software. It was the architecture: I was running my storage as a VM on my compute host. Every reboot was a roll of the dice for timing and mount order.&lt;/p&gt;
&lt;p&gt;So, I pulled the HBA and drives out of my Proxmox host and moved them to a dedicated bare metal Debian box running MergerFS with 93TB of drives and SMB and NFS shares.&lt;/p&gt;
&lt;p&gt;That was six months ago. I&amp;rsquo;ve rebooted my compute host 30+ times since then. Zero mount failures, race conditions, or extended debugging sessions late at night. My media is served up when I need it.&lt;/p&gt;
&lt;p&gt;If your media library matters, give it its own box. I go over how I built mine and why, and why you should dedicate a box to it as well.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you are trying to figure out the best NAS setup for a home media server, here is the short version: stop running your storage in a VM and give it its own bare metal box that exists only to keep your files safe and available when you want them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Running storage in a VM or LXC is like playing with fire. You think you&amp;rsquo;re being efficient. You&amp;rsquo;re actually creating race conditions, mount failures, and late night debugging sessions when you could be enjoying your media with friends and/or family. Separate storage to bare metal. It boots quickly, serves reliably, and stops holding your media hostage.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="quick-picks-best-nas-options-for-media-servers-2025"&gt;Quick picks: best NAS options for media servers (2025)
&lt;/h2&gt;&lt;p&gt;If you came here looking for the &amp;ldquo;tell me what home NAS to buy&amp;rdquo; version, here&amp;rsquo;s the short list.&lt;/p&gt;
&lt;h3 id="1-diy-bare-metal-nas-for-homelab-nerds"&gt;1. DIY bare metal NAS for homelab nerds
&lt;/h3&gt;&lt;p&gt;You want control, flexibility, and better hardware for the money.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Debian or another solid Linux base, with &lt;strong&gt;MergerFS&lt;/strong&gt; for pooling and &lt;strong&gt;NFS&lt;/strong&gt; or &lt;strong&gt;SMB&lt;/strong&gt; exports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use case&lt;/strong&gt;: Jellyfin or Plex library, backups, maybe a bit of general file storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why it is the best fit&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;You are not locked into a vendor GUI&lt;/li&gt;
&lt;li&gt;You can pick quiet, low power parts instead of whatever the NAS vendor felt like shipping&lt;/li&gt;
&lt;li&gt;Easy to grow storage with bigger drives later rather than buying a whole new box&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the setup I use and for me, it is the best NAS for a home media server. The Proxmox host talks to it over the network, and the NAS does one job and does it well.&lt;/p&gt;
&lt;h3 id="2-synology-nas-for-people-who-want-easy-mode"&gt;2. Synology NAS for people who want easy mode
&lt;/h3&gt;&lt;p&gt;You want something that works out of the box, and you don&amp;rsquo;t want to learn MergerFS.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Example&lt;/strong&gt;: 4 to 8 bay Synology DiskStation for media and backup&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use case&lt;/strong&gt;: You want a simple web UI, snapshots, built in apps, and clean integration with Windows and macOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why it is a good fit&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Synology handles RAID, drive health alerts, and shares for you&lt;/li&gt;
&lt;li&gt;Great if you want to spend more money on hardware and less time learning Linux&lt;/li&gt;
&lt;li&gt;Perfect for &amp;ldquo;I run Jellyfin on another box, this thing only holds the files&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You give up some flexibility and pay a premium compared to DIY, but you get a nice, polished experience.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C8S7SF4B"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/synology-ds925_hu_eda617831103a258.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/synology-ds925_hu_7d3ee7d460962e5f.webp" width="600" height="427" alt="Synology DiskStation DS925&amp;#43; 4-Bay NAS (Diskless)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Synology 4-Bay DiskStation DS925+ (Diskless)&lt;/strong&gt;&lt;br&gt;
Is a 4-bay NAS running DSM with an easy setup, dual 2.5 GbE, M.2 NVMe slots for cache, ECC-capable RAM up to 32 GB, expansion to 9 bays, and roughly 500+ MB/s for multi-user streaming. Pick it over a DIY build if you want simple and reliable, since DSM gives you polished wizards, built-in backup and media apps, and hardware that works out of the box.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/44ahqAM" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="3-used-server-or-small-business-box-if-you-like-deals"&gt;3. Used server or small business box if you like deals
&lt;/h3&gt;&lt;p&gt;You are comfortable with louder gear and you want lots of bays for cheap.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Examples&lt;/strong&gt;: Used Dell, HP, or Lenovo small servers or business desktops with extra SATA added&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use case&lt;/strong&gt;: Big media libraries, lots of drives, budget conscious builds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why it can make sense&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Older enterprise gear is cheap and still very capable&lt;/li&gt;
&lt;li&gt;Easy to stuff with drives and treat it as a dedicated storage tank&lt;/li&gt;
&lt;li&gt;Run Debian or your favorite NAS OS and treat it like the DIY option above&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You do have to watch power draw and noise. Great for a basement rack, not so great for a studio apartment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No matter which path you pick, the rule is the same:&lt;br&gt;
Your media server should talk to a dedicated NAS box over the network, not share a boot drive with Proxmox and six LXCs that all panic if a mount is late.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="storage-in-vms-because-you-can-doesnt-mean-you-should"&gt;Storage In VMs: Because You &amp;ldquo;CAN&amp;rdquo; Doesn&amp;rsquo;t Mean You Should
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the short version. Running your NAS in a Proxmox VM is a bad idea. Running it in an LXC is even worse.&lt;/p&gt;
&lt;p&gt;Reddit will tell you: &amp;ldquo;It&amp;rsquo;s more efficient!&amp;rdquo; or &amp;ldquo;One box does everything, and it is amazing!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m here to tell you they are wrong and what you&amp;rsquo;ve built is a house of cards where storage, the foundation of your entire media stack depends on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A hypervisor booting correctly&lt;/li&gt;
&lt;li&gt;VM startup order being predictable&lt;/li&gt;
&lt;li&gt;Mounts being added and mounted to the Proxmox host correctly (My biggest issue)&lt;/li&gt;
&lt;li&gt;Network initialization happening before mount attempts&lt;/li&gt;
&lt;li&gt;No Proxmox updates changing any of the above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;re not being efficient. You&amp;rsquo;re optimizing for a $200 hardware savings while trading it for hours of maintenance, failed mounts, and streams that don&amp;rsquo;t work when you want them to.&lt;/p&gt;
&lt;p&gt;I know because I did this for six months. I spent roughly 3 hours every week debugging mount failures, tweaking systemd scripts, adjusting delays, and reading Proxmox forums instead of watching anything. That&amp;rsquo;s 78 hours over six months (Almost two full work weeks fighting an architecture that was fundamentally wrong).&lt;/p&gt;
&lt;p&gt;The worst part? Each failure was only a 10-15 minute fix. But they happened constantly. Reboot the host? Roll the dice. Proxmox update? Hope your mounts still work. Add a new container? Maybe it boots before the NAS is ready, maybe it doesn&amp;rsquo;t.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
The root issue is that your media server runs in a VM on the same Proxmox host that&amp;rsquo;s trying to mount its shares. During boot, Proxmox processes &lt;code&gt;/etc/fstab&lt;/code&gt; and can&amp;rsquo;t mount the shares because the VM hasn&amp;rsquo;t even started. Mounting shares later isn&amp;rsquo;t as straightforward as it sounds. Any workarounds you apply afterward only treat the symptoms without addressing the root cause. I exhausted every solution I could find across Google, Reddit, and the Proxmox forums. Some provided marginal improvements, but none fully resolved the problem.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="the-real-cost"&gt;The Real Cost
&lt;/h3&gt;&lt;p&gt;Let me break down what this &amp;ldquo;efficient&amp;rdquo; setup cost me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;78+ hours of debugging&lt;/strong&gt; over six months&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple service outages&lt;/strong&gt; where Jellyfin showed &amp;ldquo;Library Empty&amp;rdquo; because mounts succeeded but pointed to empty directories&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constant user complaints&lt;/strong&gt; about streams dying or libraries disappearing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mental overhead&lt;/strong&gt; of &amp;ldquo;will this reboot break everything?&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;What did I gain by running storage in a VM? Absolutely nothing. I saved a small amount of money on power, not enough to make up for my time. That&amp;rsquo;s it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-time-my-storage-failed-me-and-why-i-finally-fixed-it"&gt;The Time My Storage Failed Me (And Why I Finally Fixed It)
&lt;/h2&gt;&lt;p&gt;Here is what finally convinced me to do this right.&lt;/p&gt;
&lt;h3 id="the-reboot-lottery"&gt;The Reboot Lottery
&lt;/h3&gt;&lt;p&gt;Power outage, kernel update, Proxmox update, stuck containers can all cause the need to reboot the Proxmox host. When the host is rebooted&amp;hellip; Proxmox comes back up. NAS VM starts&amp;hellip; but something&amp;rsquo;s off. The VM boots, the drives mount inside the VM, but Proxmox&amp;rsquo;s automount fails to see the NFS share. Jellyfin LXC mounts &lt;code&gt;/mnt/media&lt;/code&gt; to an empty local directory. Users see &amp;ldquo;Library Empty.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I SSH in, manually remount, restart containers. Fixed in 10 minutes.&lt;/p&gt;
&lt;p&gt;But this wasn&amp;rsquo;t a one-time thing. This happened a few times a week with different variations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sometimes the VM booted too slowly&lt;/li&gt;
&lt;li&gt;Sometimes Proxmox tried to mount before the NFS server was actually serving&lt;/li&gt;
&lt;li&gt;Sometimes it worked perfectly&lt;/li&gt;
&lt;li&gt;Sometimes my custom delay scripts helped, sometimes they didn&amp;rsquo;t&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Systemd mount delays (worked sometimes)&lt;/li&gt;
&lt;li&gt;Custom scripts that pinged the NAS before mounting (race conditions remained)&lt;/li&gt;
&lt;li&gt;Automount with longer timeouts (helped but didn&amp;rsquo;t fix it)&lt;/li&gt;
&lt;li&gt;Tweaking VM boot order and priority (marginal improvement)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nothing worked reliably. The configuration wasn&amp;rsquo;t the problem. The architecture was.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when I realized: this is insane. I&amp;rsquo;m spending hours maintaining mount orchestration on a system that should &amp;ldquo;work without babysitting.&amp;rdquo; The storage shouldn&amp;rsquo;t be a VM. It shouldn&amp;rsquo;t depend on a hypervisor. It should boot first and serve files. Period.&lt;/p&gt;
&lt;h2 id="the-vm-race-condition-from-hell-whats-happening"&gt;The VM Race Condition From Hell (What&amp;rsquo;s Happening)
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what was happening in my Proxmox host every reboot:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Proxmox boots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VMs start launching&lt;/strong&gt; - based on boot order/priority settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;My NAS VM begins booting&lt;/strong&gt; - this takes time: OS boot, network init, NFS server start&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LXC containers start&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Containers try to mount &lt;code&gt;/mnt/media&lt;/code&gt; via NFS&lt;/strong&gt; from the Proxmox host (Bind mounts)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Three possible outcomes:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;✅ NAS is ready → mount succeeds → everything works&lt;/li&gt;
&lt;li&gt;❌ NAS isn&amp;rsquo;t ready → mount fails → services break&lt;/li&gt;
&lt;li&gt;⚠️ Mount succeeds but NFS isn&amp;rsquo;t serving yet → mount points to empty directory → &amp;ldquo;Library Empty&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That third one is the nastiest because everything LOOKS like it worked. The mount command succeeded. The directory exists. But there&amp;rsquo;s no data because the NFS server inside the VM hasn&amp;rsquo;t finished starting yet.&lt;/p&gt;
&lt;h3 id="why-this-is-fundamentally-broken"&gt;Why This Is Fundamentally Broken
&lt;/h3&gt;&lt;p&gt;The hypervisor doesn&amp;rsquo;t know or care about your application dependencies. Proxmox sees:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAS VM (priority: normal)&lt;/li&gt;
&lt;li&gt;App containers (priority: normal)&lt;/li&gt;
&lt;li&gt;Network is up&lt;/li&gt;
&lt;li&gt;Start everything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It doesn&amp;rsquo;t understand that your Jellyfin container NEEDS the NAS to be fully operational before it can function. You can try to encode this with systemd dependencies, boot delays, ping scripts, and health checks but, you&amp;rsquo;re still fighting the architecture.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The solution isn&amp;rsquo;t better orchestration. It&amp;rsquo;s removing the orchestration entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="lxcs-are-even-worse-seriously-dont"&gt;LXCs Are Even Worse (Seriously, Don&amp;rsquo;t)
&lt;/h2&gt;&lt;p&gt;I never tried running my NAS in an LXC. But I&amp;rsquo;ve seen people attempt it on Reddit and Proxmox forums, and it&amp;rsquo;s a nightmare every single time.&lt;/p&gt;
&lt;p&gt;Why people try it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;LXCs are lighter than VMs!&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;I don&amp;rsquo;t need full VM overhead for a file server!&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;I can bind-mount drives directly!&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why it fails spectacularly:&lt;/p&gt;
&lt;h3 id="no-real-hardware-access"&gt;No Real Hardware Access
&lt;/h3&gt;&lt;p&gt;LXCs share the host kernel. You can pass through devices, but you&amp;rsquo;re not getting true hardware access like bare metal. Running ZFS in an LXC? You&amp;rsquo;re trusting the container layer to not screw up your file systems. Running SMART monitoring? Good luck with device passthrough being consistent.&lt;/p&gt;
&lt;h3 id="the-same-mount-race-conditions-but-worse"&gt;The Same Mount Race Conditions, But Worse
&lt;/h3&gt;&lt;p&gt;The LXC has to start, the storage daemon has to initialize, the NFS/SMB server has to start, and THEN other containers can mount. You&amp;rsquo;ve recreated the VM problem with less isolation and more ways for it to break.&lt;/p&gt;
&lt;h3 id="what-happens"&gt;What Happens
&lt;/h3&gt;&lt;p&gt;Search r/Proxmox for &amp;ldquo;LXC NFS&amp;rdquo; or &amp;ldquo;LXC storage&amp;rdquo; and you&amp;rsquo;ll find a number of posts like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;My LXC can&amp;rsquo;t see the drives after reboot&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Permissions are broken after Proxmox update&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;SMART data isn&amp;rsquo;t available in the container&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The pattern is always the same:&lt;/strong&gt; someone tries to be clever, runs storage in an LXC to &amp;ldquo;save resources,&amp;rdquo; and ends up with a fragile, unreliable mess.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t do it.&lt;/p&gt;
&lt;h2 id="why-dedicated-bare-metal-is-the-only-sane-option"&gt;Why Dedicated Bare Metal Is The Only Sane Option
&lt;/h2&gt;&lt;p&gt;When I moved my storage to bare metal Debian, here&amp;rsquo;s what changed:&lt;/p&gt;
&lt;h3 id="predictable-boot-order-finally"&gt;Predictable Boot Order (Finally)
&lt;/h3&gt;&lt;p&gt;It boots, drives spin up, XFS filesystems mount, MergerFS pools them, NFS server starts. All of this happens BEFORE my Proxmox host even starts booting.&lt;/p&gt;
&lt;p&gt;By the time Proxmox comes online and containers try to mount &lt;code&gt;/mnt/media&lt;/code&gt;, the NAS has already been serving files and waiting. Zero race conditions, timing dependencies, or failed mounts.&lt;/p&gt;
&lt;h3 id="no-more-orchestration-hell"&gt;No More Orchestration Hell
&lt;/h3&gt;&lt;p&gt;I deleted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom systemd mount units with delays&lt;/li&gt;
&lt;li&gt;Ping-before-mount scripts&lt;/li&gt;
&lt;li&gt;VM boot order priorities&lt;/li&gt;
&lt;li&gt;Health check containers&lt;/li&gt;
&lt;li&gt;All the &amp;ldquo;clever&amp;rdquo; solutions I built to work around a broken architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The NAS boots. It serves files. Proxmox mounts them. That&amp;rsquo;s it. No orchestration needed.&lt;/p&gt;
&lt;h3 id="fault-isolation"&gt;Fault Isolation
&lt;/h3&gt;&lt;p&gt;When I need to rebuild my Proxmox host or test new versions, my storage stays online. It keeps serving files to the containers that are still running. When I need to add drives or run maintenance on the NAS, I take it offline briefly, but my compute layer isn&amp;rsquo;t impacted. I do reboot the compute node after NAS maintenance to confirm everything reconnects cleanly.&lt;/p&gt;
&lt;p&gt;Before, everything was tangled together. Proxmox down = storage down. Storage issues = compute issues. It was all one fragile system. I made it even worse because at the time my router was also in a VM on the same Proxmox host. So, every reboot also took down my internet too. Don&amp;rsquo;t be like me.&lt;/p&gt;
&lt;h3 id="zero-maintenance"&gt;Zero Maintenance
&lt;/h3&gt;&lt;p&gt;In eight months since the migration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proxmox reboots:&lt;/strong&gt; 30+&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAS mount failures:&lt;/strong&gt; 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hours spent debugging storage:&lt;/strong&gt; 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streams interrupted by storage issues:&lt;/strong&gt; 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The NAS works. I literally forget it exists until I need to add more drives.&lt;/p&gt;
&lt;h3 id="what-i-gave-up"&gt;What I Gave Up
&lt;/h3&gt;&lt;p&gt;Nothing. Absolutely nothing.&lt;/p&gt;
&lt;p&gt;I added one more box to my rack (an old desktop PC I already owned). That&amp;rsquo;s it. No performance penalty. No feature loss. No additional complexity. Actually, LESS complexity because I removed all the mount orchestration.&lt;/p&gt;
&lt;p&gt;Adding a low power CPU to run the NAS had a minimal impact to the power bill (About 40-60W).&lt;/p&gt;
&lt;div class="product-box" data-asin="B07H289S7C"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate-8tb_hu_3ac46966354d0ce6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate-8tb_hu_892fe1aa3bbbb134.webp" width="600" height="861" alt="Seagate BarraCuda Internal Hard Drive 8TB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate BarraCuda Internal Hard Drive 8TB&lt;/strong&gt;&lt;br&gt;
Right now one of the best price per GB you can find. Also has a 2-year warranty.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/45Cw97V" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="but-my-nas-vm-works-fine-for-now"&gt;&amp;ldquo;But My NAS VM Works Fine&amp;rdquo; (For Now)
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re reading this thinking &amp;ldquo;my NAS VM is stable, this doesn&amp;rsquo;t apply to me,&amp;rdquo; let me ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Have you rebooted your Proxmox host this month? Did everything come back up cleanly?&lt;/li&gt;
&lt;li&gt;Have you updated Proxmox recently? Did your mounts still work after?&lt;/li&gt;
&lt;li&gt;Do you have multiple containers depending on storage? Do they all mount reliably?&lt;/li&gt;
&lt;li&gt;Have you tested what happens during a power failure and cold boot?&lt;/li&gt;
&lt;li&gt;Can you reboot your compute layer without taking storage offline?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you answered &amp;ldquo;yes&amp;rdquo; to all of these, congratulations! You&amp;rsquo;ve either gotten extremely lucky or you&amp;rsquo;ve spent dozens of hours building complex orchestration to paper over the architectural problems.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the thing: it works until it doesn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re not running a reliable system. You&amp;rsquo;re running a system that hasn&amp;rsquo;t failed YET. And when it does fail during a family movie night, you&amp;rsquo;ll wish you&amp;rsquo;d built it right from the start.&lt;/p&gt;
&lt;h3 id="the-real-question"&gt;The Real Question
&lt;/h3&gt;&lt;p&gt;What are you gaining by running storage in a VM?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Saving one hardware box? (An old desktop PC costs $50-200 used)&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Efficiency&amp;rdquo;? (You&amp;rsquo;re trading hardware efficiency for operational chaos)&lt;/li&gt;
&lt;li&gt;Easier management? (Is debugging mount failures &amp;ldquo;easier&amp;rdquo; than running a separate box you never touch?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Be honest: you&amp;rsquo;re not optimizing for reliability or simplicity. You&amp;rsquo;re optimizing for&amp;hellip; what, exactly?&lt;/p&gt;
&lt;h2 id="the-architecture-comparison"&gt;The Architecture Comparison
&lt;/h2&gt;&lt;p&gt;Let me show you what I ran vs what I run now.&lt;/p&gt;
&lt;h3 id="wrong-what-i-built-first"&gt;WRONG: What I Built First
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Proxmox Host (one physical box)
├── NAS VM (OMV)
│ ├── Boots at ??? (depends on VM priority)
│ ├── Needs: OS boot → network init → storage mount → NFS/SMB start
│ └── Serves: NFS/SMB shares back to Proxmox host
│
├── Jellyfin LXC
│ ├── Boots at ??? (fast, because LXC)
│ ├── Tries to mount: /mnt/media (from Proxmox host bind mount)
│ └── Result: ⚠️ Maybe works, maybe empty directory, maybe fails
│
├── Sonarr LXC
│ ├── Boots at ???
│ ├── Tries to mount: /mnt/media (from Proxmox host bind mount)
│ └── Result: ⚠️ Maybe works, maybe empty directory, maybe fails
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boot order is non-deterministic (depends on VM/LXC startup speed)&lt;/li&gt;
&lt;li&gt;NAS VM has multiple initialization steps before it&amp;rsquo;s ready to serve&lt;/li&gt;
&lt;li&gt;Containers mount before checking if NFS is serving&lt;/li&gt;
&lt;li&gt;Reboots are a lottery&lt;/li&gt;
&lt;li&gt;Proxmox updates can change everything (rare, but still a thing)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="right-what-i-run-now"&gt;RIGHT: What I Run Now
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;NAS Box (dedicated bare metal Debian)
├── Hardware: Intel G3220, 16GB RAM, LSI HBA, 10GbE NIC
├── Software: Debian Trixie, XFS per drive, MergerFS pooling
├── Boot: Second. After the dedicated Router
└── Serves: NFS shares to network (always available)
↓ (2.5GbE Network)
Proxmox Host (separate physical box)
├── Boot: Third (after NAS is already serving)
├── Mounts: /mnt/media via NFS from NAS box (always succeeds)
│
├── Jellyfin LXC → bind mounts /mnt/media → ✅ always works
└── Sonarr LXC → bind mounts /mnt/media → ✅ always works
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boot order is deterministic (NAS second, always)&lt;/li&gt;
&lt;li&gt;No dependencies between storage and compute layers&lt;/li&gt;
&lt;li&gt;Reboots are predictable&lt;/li&gt;
&lt;li&gt;Proxmox updates don&amp;rsquo;t affect storage&lt;/li&gt;
&lt;li&gt;Fault isolation (one system failing doesn&amp;rsquo;t take down the other)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="network-details"&gt;Network Details
&lt;/h3&gt;&lt;p&gt;Connected via SODOLA 8-Port 2.5Gb managed switch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAS: 10GbE NIC (downlinks at 2.5Gb to switch)&lt;/li&gt;
&lt;li&gt;Proxmox: 2.5GbE NIC&lt;/li&gt;
&lt;li&gt;Throughput: ~280 MB/s sustained (vs ~110 MB/s on 1GbE)&lt;/li&gt;
&lt;li&gt;Multiple simultaneous 4K streams + NZB downloads + backups = no congestion&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-i-migrated"&gt;How I Migrated
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s exactly how I moved from &amp;ldquo;NAS VM on Proxmox&amp;rdquo; to &amp;ldquo;NAS on bare metal.&amp;rdquo;&lt;/p&gt;
&lt;h3 id="what-i-started-with"&gt;What I Started With
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Proxmox host with LSI HBA in IT mode&lt;/li&gt;
&lt;li&gt;8x drives passed through to OMV VM&lt;/li&gt;
&lt;li&gt;Containers mounting via NFS from Proxmox host (bind mounts)&lt;/li&gt;
&lt;li&gt;Constant race condition issues&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-i-did"&gt;What I Did
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1. Built the new NAS box first&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Old desktop: Intel G3220, Gigabyte GA-Z87X-D3H motherboard, 16GB DDR3&lt;/li&gt;
&lt;li&gt;Installed Debian Trixie&lt;/li&gt;
&lt;li&gt;Added 10GbE NIC (Intel X520-DA1, $30 used on eBay)&lt;/li&gt;
&lt;li&gt;Configured it on the network with a static IP&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Tested NFS serving (before moving drives)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up NFS exports on the new Debian box&lt;/li&gt;
&lt;li&gt;Verified Proxmox could mount from it&lt;/li&gt;
&lt;li&gt;Made sure permissions and paths matched my existing setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Scheduled downtime (Friday night)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shut down all containers/VMs on Proxmox&lt;/li&gt;
&lt;li&gt;Shut down the NAS VM&lt;/li&gt;
&lt;li&gt;Shut down the Proxmox host&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Moved the hardware&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pulled the LSI HBA from Proxmox host&lt;/li&gt;
&lt;li&gt;Moved all 8 drives&lt;/li&gt;
&lt;li&gt;Installed HBA and drives in the new NAS box&lt;/li&gt;
&lt;li&gt;Connected power, network&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. Brought up storage&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Booted the NAS box&lt;/li&gt;
&lt;li&gt;Drives appeared as &lt;code&gt;/dev/sda&lt;/code&gt; through &lt;code&gt;/dev/sdh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Mounted each drive: &lt;code&gt;mount /dev/sdX /mnt/diskX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Configured MergerFS to pool them: &lt;code&gt;/mnt/disk* /mnt/media&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set up NFS exports pointing to &lt;code&gt;/mnt/media&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Updated &lt;code&gt;/etc/fstab&lt;/code&gt; so everything mounts on boot&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;6. Brought Proxmox back online&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Booted Proxmox&lt;/li&gt;
&lt;li&gt;Updated &lt;code&gt;/etc/fstab&lt;/code&gt; to point to the new NAS IP instead of VM mount&lt;/li&gt;
&lt;li&gt;Verified NFS mounts succeeded&lt;/li&gt;
&lt;li&gt;Started containers one by one&lt;/li&gt;
&lt;li&gt;Checked Jellyfin library, verified all media was accessible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Total downtime: ~2 hours&lt;/strong&gt; (most of it was physical drive transplant and cable management)&lt;/p&gt;
&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/lsi-9300-8i_hu_d90f79977888bf33.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/lsi-9300-8i_hu_457d5d766f037cc6.webp" width="600" height="600" alt="LSI 9300-8i HBA Controller" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;LSI 9300-8i HBA Controller&lt;/strong&gt;&lt;br&gt;
LSI SAS3008 9300-8i HBA Controller is a high-performance disk controller suitable for media server data storage needs. With a transfer rate of up to 12Gbps, this unit provides reliable and fast data storage solutions.
&lt;strong&gt;Price&lt;/strong&gt;: $30 - $40
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=LSI&amp;#43;9300-8i&amp;#43;IT&amp;#43;Mode&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="what-i-learned"&gt;What I Learned
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Test the new setup before you commit.&lt;/strong&gt; I verified NFS serving worked before I moved drives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expect the first boot to take longer.&lt;/strong&gt; XFS file system checks took a few minutes per drive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for worst case.&lt;/strong&gt; I kept the Proxmox host ready to take the HBA back if something went catastrophically wrong (it didn&amp;rsquo;t).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-your-nas-needs-to-do"&gt;What Your NAS Needs To Do
&lt;/h2&gt;&lt;h3 id="redundancy"&gt;Redundancy
&lt;/h3&gt;&lt;p&gt;At minimum: ability to survive drive failure without data loss.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be honest: I ran without parity for two years. Why? Because 95% of my data was movies and TV shows I could redownload. Then I started adding family videos and photos - stuff I can&amp;rsquo;t get back. That&amp;rsquo;s when I decided to add SnapRAID for parity (I&amp;rsquo;m adding this next month if HDD prices come down).&lt;/p&gt;
&lt;p&gt;Use any scheme you&amp;rsquo;re comfortable with: parity, mirrors, or redundancy only for the critical bits. Know what you&amp;rsquo;re protecting and what you&amp;rsquo;re willing to lose.&lt;/p&gt;
&lt;h3 id="protocols-that-work"&gt;Protocols That Work
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NFS&lt;/strong&gt; for your virtualization or Linux services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMB/CIFS&lt;/strong&gt; for Windows/macOS clients&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFTP&lt;/strong&gt; for remote access or backup jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your NAS needs to plug into your compute and network layers without drama. My setup serves NFS to Proxmox hosts and SMB to Windows machines for manual file management. No fuss.&lt;/p&gt;
&lt;h3 id="smart-mounting-strategy"&gt;Smart Mounting Strategy
&lt;/h3&gt;&lt;p&gt;This is where most people screw up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stick to consistent paths&lt;/strong&gt;: &lt;code&gt;/mnt/media/movies&lt;/code&gt;, &lt;code&gt;/mnt/media/shows&lt;/code&gt; not &lt;code&gt;/media1&lt;/code&gt;, &lt;code&gt;/media2&lt;/code&gt;, &lt;code&gt;/random-drive-name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable automount&lt;/strong&gt; so on reboot everything comes up in order (systemd handles this well on Debian)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep temp, transcode, and download directories separate&lt;/strong&gt; from your main pool to avoid runaway writes destroying your media drives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use SMART monitoring&lt;/strong&gt; and actually test restore workflows. You&amp;rsquo;ll thank yourself later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I run &lt;code&gt;smartctl -a /dev/sdX&lt;/code&gt; monthly on each drive. One drive showed reallocated sectors climbing. Replaced it before it died.&lt;/p&gt;
&lt;h2 id="software-choices-what-ive-used-and-what-works"&gt;Software Choices: What I&amp;rsquo;ve Used and What Works
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what I&amp;rsquo;ve tested and what I run.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;NAS OS&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Weaknesses&lt;/th&gt;
&lt;th&gt;My Take&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TrueNAS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strong ZFS support, snapshot capabilities, enterprise features&lt;/td&gt;
&lt;td&gt;Needs more RAM/hardware, steeper learning curve, overkill for media&lt;/td&gt;
&lt;td&gt;Great if you need ZFS. I don&amp;rsquo;t.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unraid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flexible drives, less hardware-intensive, nice GUI&lt;/td&gt;
&lt;td&gt;License cost ($59-$129), lower performance on some tasks&lt;/td&gt;
&lt;td&gt;Popular for good reason, but I&amp;rsquo;m cheap and wanted full control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenMediaVault&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight, easy to set up, web GUI&lt;/td&gt;
&lt;td&gt;May lack advanced features, felt restrictive to me&lt;/td&gt;
&lt;td&gt;Where I started in a VM, outgrew it fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debian + MergerFS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight, total control, exactly what you need&lt;/td&gt;
&lt;td&gt;No GUI (you use SSH and config files), learning curve&lt;/td&gt;
&lt;td&gt;This is what I run. Took a weekend to learn, now it&amp;rsquo;s bulletproof.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;I originally tried OpenMediaVault in a VM. It worked, but every time I wanted to do something slightly custom, I fought with the GUI or the update system. Moved to straight Debian with MergerFS and NFS/SMB servers. No GUI. Config files and systemd. Steeper learning curve? Yes. But now I understand exactly how everything works and nothing is hidden behind abstraction layers. When something breaks (rare), I know where to look.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Whatever you choose, run it on bare metal. Avoid running it in a VM or LXC. You will thank yourself later.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="zfs-vs-mergerfs---whats-best-for-a-media-server"&gt;ZFS vs MergerFS - What&amp;rsquo;s Best For a Media Server?
&lt;/h2&gt;&lt;p&gt;Cut the fluff. You don&amp;rsquo;t always need ZFS for a media-server-only setup.&lt;/p&gt;
&lt;h3 id="zfs-enterprise-grade-redundancy"&gt;ZFS: Enterprise Grade Redundancy
&lt;/h3&gt;&lt;p&gt;RAID-Z/RAID-Z2, checksums, snapshots, send/receive. Requires serious RAM (1GB per TB is the common recommendation) and prefers ECC memory.&lt;/p&gt;
&lt;p&gt;I almost started with ZFS because that&amp;rsquo;s what the internet said to use. Researched ECC RAM, planned my vdev layout, read the entire FreeBSD handbook section on ZFS. Felt very enterprise. It also had an enterprise price tag.&lt;/p&gt;
&lt;p&gt;Then I realized: if a drive dies, I can re-download everything in a weekend. Why am I treating Bob&amp;rsquo;s Burgers S04 like a production database?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When ZFS is worth it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re running VMs, databases, things that can&amp;rsquo;t be redownloaded&lt;/li&gt;
&lt;li&gt;You can afford to buy 6 or more identical drives in one purchase (I couldn&amp;rsquo;t justify this).&lt;/li&gt;
&lt;li&gt;You have irreplaceable data (family photos, business files)&lt;/li&gt;
&lt;li&gt;You want snapshots and send/receive for backups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For me?&lt;/strong&gt; 80% of my data was re-downloadable movies and shows. ZFS was overkill. I make backups of the irreplaceable files and keep them in three locations (Local, External HDD, and Cloud).&lt;/p&gt;
&lt;h3 id="mergerfs-flexible-media-friendly"&gt;MergerFS: Flexible, Media Friendly
&lt;/h3&gt;&lt;p&gt;MergerFS pools drives into &lt;code&gt;/mnt/media&lt;/code&gt; regardless of size.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it works for media:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add drives without rebuilding (I started with 3 drives, now at 8)&lt;/li&gt;
&lt;li&gt;Any size, any speed, different brands, no &amp;ldquo;matched set&amp;rdquo; needed&lt;/li&gt;
&lt;li&gt;If a drive dies, you lose only what was on that drive, not the whole pool&lt;/li&gt;
&lt;li&gt;Reads are fast, writes go to whichever drive has space (configurable policies)&lt;/li&gt;
&lt;li&gt;Works on bare metal Debian with zero virtualization overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; No real-time parity. If a drive dies, that data is gone unless you have backups or add parity separately (see SnapRAID below).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ZFS&lt;/th&gt;
&lt;th&gt;MergerFS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time parity&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ Not built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexible drive sizes&lt;/td&gt;
&lt;td&gt;❌ No (same-size vdevs)&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-level recovery&lt;/td&gt;
&lt;td&gt;❌ Generally no&lt;/td&gt;
&lt;td&gt;✅ Straightforward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware overhead&lt;/td&gt;
&lt;td&gt;High (RAM, ECC preferred)&lt;/td&gt;
&lt;td&gt;Low (runs on anything)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM-friendly&lt;/td&gt;
&lt;td&gt;⚠️ Possible but problematic&lt;/td&gt;
&lt;td&gt;✅ But run it on bare metal anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal for media&lt;/td&gt;
&lt;td&gt;Overkill&lt;/td&gt;
&lt;td&gt;Perfect fit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your NAS is mostly media you can re-download - go MergerFS&lt;/li&gt;
&lt;li&gt;If your NAS hosts business-critical or irreplaceable data - go ZFS&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mergerfs--snapraid-the-best-of-both-worlds"&gt;MergerFS + SnapRAID: The Best of Both Worlds
&lt;/h3&gt;&lt;p&gt;MergerFS gives me flexible pooling. But what about redundancy?&lt;/p&gt;
&lt;p&gt;Enter SnapRAID: parity for files that don&amp;rsquo;t change often (perfect for media).&lt;/p&gt;
&lt;p&gt;How it works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I dedicate 1 drive as a parity drive (the largest drive)&lt;/li&gt;
&lt;li&gt;SnapRAID calculates parity across the pool on-demand (I&amp;rsquo;ll run it nightly via cron)&lt;/li&gt;
&lt;li&gt;If a drive dies, I can rebuild from parity&lt;/li&gt;
&lt;li&gt;Unlike RAID, parity is calculated when YOU tell it to, not in real-time&lt;/li&gt;
&lt;li&gt;If TWO drives die before I sync parity&amp;hellip; yeah, I lose some files. But that&amp;rsquo;s the trade-off for flexibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m adding this to my setup next month because I finally have data I can&amp;rsquo;t easily redownload (family videos, photos). For movies and TV? I didn&amp;rsquo;t bother for two years. The cost/benefit wasn&amp;rsquo;t there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this works on bare metal:&lt;/strong&gt; SnapRAID needs direct drive access for parity calculation. Running it in a VM means the hypervisor sits between your file system and the drives, adding latency, complexity, and potential corruption. On bare metal? No layers in the way.&lt;/p&gt;
&lt;h2 id="hardware-recommendations"&gt;Hardware Recommendations
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s gear time. Because yes, you can buy this now. And yes, you can target budget or beast mode depending on how deep your wallet is.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Specs&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;What I&amp;rsquo;d Buy Today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Budget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-4 bays, low-power CPU, basic RAM&lt;/td&gt;
&lt;td&gt;Cold storage, archives&lt;/td&gt;
&lt;td&gt;Old desktop with 2-4 SATA ports. Intel Pentium or i3, 8GB RAM. Purpose: hold files, serve NFS. Cost: $50-100 used.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Balanced&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4-8 bays, decent CPU, 16GB RAM&lt;/td&gt;
&lt;td&gt;Streaming + moderate load&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Close to my setup.&lt;/strong&gt; Rosewill Helium NAS case ($90), Intel G3220 or newer i3 ($50 used), 16GB RAM, LSI HBA in IT mode ($50 used), 8x drives. Quiet, expandable, fits under a desk. Total: ~$300 + drives.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Beast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8+ bays, modern i5/i7, 10GbE NICs&lt;/td&gt;
&lt;td&gt;Multi-user, 4K/8K, heavy lift&lt;/td&gt;
&lt;td&gt;Rosewill Helium NAS or bigger case, i5-12400 or better, 32GB RAM, 10GbE NIC, quality PSU. Overkill for most, perfect if you&amp;rsquo;re streaming to 5+ users simultaneously. Cost: $600-800 + drives.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Synology 4-8 bay NAS&lt;/td&gt;
&lt;td&gt;Want it to work, don&amp;rsquo;t want to DIY&lt;/td&gt;
&lt;td&gt;DS920+, DS1522+, or whatever&amp;rsquo;s current. You pay more, but it works out of the box. No shame in this. Cost: $400-800 + drives.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="product-box" data-asin="B0DP3PQFKW"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/rosewill-helium_hu_5126aadb3fcc1746.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/rosewill-helium_hu_69bfd459d0bfd04c.webp" width="600" height="898" alt="Rosewill Helium NAS ATX Mid-Tower Case" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Rosewill Helium NAS ATX Mid-Tower Case&lt;/strong&gt;&lt;br&gt;
The Rosewill Helium NAS ATX mid-tower is a budget-friendly case built with storage in mind. It fits a standard ATX motherboard, has space for 10 3.5&amp;quot; hard drives as well as room for HBAs or SATA expanders. For a DIY NAS this case lots of room for growth without paying server-chassis prices.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0g6YNIYR" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Critical hardware note:&lt;/strong&gt; Whatever you buy, it needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direct SATA connections (no USB)&lt;/li&gt;
&lt;li&gt;Enough RAM for your OS + file system cache (8GB minimum, 16GB better)&lt;/li&gt;
&lt;li&gt;Quality power supply (drives are expensive, don&amp;rsquo;t cheap out on PSU)&lt;/li&gt;
&lt;li&gt;Network connectivity that matches your needs (2.5GbE or 10GbE if you have heavy traffic)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Not sure how many TB you need? The media storage calculator turns your movie and episode counts into a drive-buying number, with growth headroom built in.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/media-storage-calculator/"
class="backlink-button"
target="_self"
&gt;
Size your library
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="network-the-bottleneck-nobody-talks-about"&gt;Network: The Bottleneck Nobody Talks About
&lt;/h2&gt;&lt;p&gt;1GbE = 125 MB/s theoretical, ~110 MB/s real-world.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s fine for one 4K stream (40 Mbps) or even 20 simultaneous 1080p streams. Sounds like plenty, right?&lt;/p&gt;
&lt;p&gt;Except you&amp;rsquo;re also:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Downloading a new TV Series&lt;/li&gt;
&lt;li&gt;Running backups to your PBS box (Part 5 of this series)&lt;/li&gt;
&lt;li&gt;Scanning new media into Jellyfin&lt;/li&gt;
&lt;li&gt;Maybe transcoding if someone&amp;rsquo;s on a slow client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now you&amp;rsquo;re maxing out that 1GbE link. Streams buffer. Your spouse asks if the internet is broken. You check your bandwidth graphs and realize: it&amp;rsquo;s not the ISP, it&amp;rsquo;s your internal network.&lt;/p&gt;
&lt;h3 id="what-i-did-1"&gt;What I Did
&lt;/h3&gt;&lt;p&gt;Added a $30 Intel X520-DA1 10GbE NIC to my NAS box (used on eBay) and a $60 SODOLA 8-Port 2.5Gb managed switch.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0GKPNRFGF"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-8port_hu_ffc7d51ed426b5ba.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-8port_hu_512d55343887425b.webp" width="600" height="600" alt="TP-Link Omada ES210X-M2 8-Port 2.5GbE Switch" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada ES210X-M2 8-Port 2.5GbE Switch&lt;/strong&gt;&lt;br&gt;
Provides eight 2.5GbE ports and two 10G SFP+ uplinks in a quiet fanless metal chassis, with an Easy Managed web UI covering 802.1Q VLANs, IGMP snooping, and auto loop prevention. It unlocks multi-gig LAN speeds for NAS and desktops while keeping segmentation clean and power draw low.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B07USpZzq" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAS has 10GbE NIC (downlinks at 2.5Gb to the switch)&lt;/li&gt;
&lt;li&gt;Proxmox has 2.5GbE NIC&lt;/li&gt;
&lt;li&gt;Sustained throughput: ~280 MB/s (vs ~110 MB/s on 1GbE)&lt;/li&gt;
&lt;li&gt;Multiple 4K streams + torrents + backups = no congestion&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Streams never buffer. Backups run 2.5x faster. Torrents don&amp;rsquo;t fight with Jellyfin for bandwidth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you&amp;rsquo;re building from scratch:&lt;/strong&gt; Buy a motherboard with 2.5GbE built in. The Intel i226-V chipset is solid and adds $0 to motherboard cost these days. Many boards in the $100-150 range include it standard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to consider 10GbE:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5+ simultaneous users streaming 4K&lt;/li&gt;
&lt;li&gt;Heavy backup workloads (multiple TB per day)&lt;/li&gt;
&lt;li&gt;You transcode on a separate box and move large files constantly&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re a data hoarder moving TBs between systems regularly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most home setups? 2.5GbE is the sweet spot. Cheap, no special cables needed (works on Cat5e), massive improvement over 1GbE.&lt;/p&gt;
&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-x520-da1_hu_fa4a4cea79953937.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-x520-da1_hu_e761e97a639bc642.webp" width="600" height="600" alt="Intel X520-DA1 10GbE NIC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel X520-DA1 10GbE NIC&lt;/strong&gt;&lt;br&gt;
This 10Gtek network card is designed for use with Intel X520-DA1 routers and features a maximum data rate of 10 Gbps. With one SFP+ port and PCIe x8 interface, this card provides high-speed connectivity for your network.
&lt;strong&gt;Price&lt;/strong&gt;: $30 - $40
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=Intel&amp;#43;X520-DA1&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;My network setup:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NAS: Intel X520-DA1 10GbE NIC with DAC cable to switch&lt;/li&gt;
&lt;li&gt;Switch: SODOLA 8-Port 2.5Gb managed switch&lt;/li&gt;
&lt;li&gt;Proxmox: Onboard 2.5GbE&lt;/li&gt;
&lt;li&gt;Clients: Mix of 1GbE and 2.5GbE&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total cost: ~$90 for the upgrade. Best $90 I&amp;rsquo;ve spent on this build.&lt;/p&gt;
&lt;h2 id="storage-layouts-that-make-sense"&gt;Storage Layouts That Make Sense
&lt;/h2&gt;&lt;p&gt;Turning hardware into a predictable, reliable stack.&lt;/p&gt;
&lt;h3 id="directory-structure"&gt;Directory Structure
&lt;/h3&gt;&lt;p&gt;Use clear, consistent paths:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/mnt/media/
├── movies/
├── shows/
├── music/
└── books/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Not &lt;code&gt;/media1/stuff&lt;/code&gt;, &lt;code&gt;/driveB/movies&lt;/code&gt;, &lt;code&gt;/bob-likes-anime/&lt;/code&gt;. Keep it simple. Keep it consistent.&lt;/p&gt;
&lt;p&gt;Why this matters: when you&amp;rsquo;re debugging a mount that came up empty (you won&amp;rsquo;t be, because bare metal doesn&amp;rsquo;t have race conditions, but hypothetically), you want obvious paths. When you&amp;rsquo;re setting up a new container, you want to know exactly where &lt;code&gt;/mnt/media/movies&lt;/code&gt; lives.&lt;/p&gt;
&lt;h3 id="separate-temp-and-archive"&gt;Separate Temp and Archive
&lt;/h3&gt;&lt;p&gt;This is critical and most people get it wrong.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transcode, temp, and download directories&lt;/strong&gt;: Put these on SSDs or a separate spindle pool that you don&amp;rsquo;t care about. You do NOT want qBittorrent hammering your archive drives with random writes while incomplete downloads get moved around.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finished media&lt;/strong&gt;: Goes to the archive pool (the big slow drives via MergerFS). This is read-mostly workload. Movies get added once, read many times, rarely deleted.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My setup:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;NAS Box:
├── 500GB SSD: Debian 13, NFS, SMB, MergerFS and Snapraid (that&amp;#39;s it don&amp;#39;t over complicate it).
└── 93TB MergerFS pool (Mix of 3, 6, 14, and 24TB Drives): Static Storage only.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Any Docker containers, downloads, or Jellyfin transcodes are on the compute node with fast redundant data storage.&lt;/p&gt;
&lt;p&gt;SABnzbd downloads to compute node&amp;rsquo;s fast storage. When a download completes, Sonarr/Radarr move it to &lt;code&gt;/mnt/media/tv&lt;/code&gt; or &lt;code&gt;/mnt/media/movies&lt;/code&gt; on the MergerFS pool. The compute node&amp;rsquo;s storage absorbs all the random write punishment. The spinning drives handle sequential writes when media is added and sequential reads when streaming.&lt;/p&gt;
&lt;h3 id="mount-strategy"&gt;Mount Strategy
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mount read-only where possible&lt;/strong&gt; for older content (reduces risk of accidental deletion). My Jellyfin server can only read the media files on the NAS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up rsync or backups&lt;/strong&gt; to your backup box (Part 5 of this series) so you&amp;rsquo;re not relying on storage redundancy for your important files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run SMART monitoring&lt;/strong&gt;: &lt;code&gt;smartctl -a /dev/sdX&lt;/code&gt; monthly, check for reallocated sectors, pending sectors, or UDMA CRC errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schedule pool scrubs&lt;/strong&gt; if using SnapRAID (I&amp;rsquo;ll run &lt;code&gt;snapraid sync&lt;/code&gt; nightly, &lt;code&gt;snapraid scrub&lt;/code&gt; weekly once I set it up)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test restores&lt;/strong&gt;: Seriously. Shut down, pull a drive, boot, verify you know how to identify which drive failed and how MergerFS handles it. Learn how recovery works BEFORE a drive fails on you.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;When your storage setup is done well - you forget it&amp;rsquo;s there. When it fails - you will be ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="my-actual-mergerfs-config"&gt;My Actual MergerFS Config
&lt;/h3&gt;&lt;p&gt;For reference, here&amp;rsquo;s what I run on bare metal Debian:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drive mounts in &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/dev/disk/by-id/wwn-0x5000c500b531bbcc-part1 /mnt/Pool/Disk1 xfs defaults 0 0
/dev/disk/by-id/wwn-0x5000c500e4505355-part1 /mnt/Pool/Disk2 xfs defaults 0 0
/dev/disk/by-id/wwn-0x5000c500e50b9986-part1 /mnt/Pool/Disk3 xfs defaults 0 0
/dev/disk/by-id/wwn-0x5000c500e82476d9-part1 /mnt/Pool/Disk4 xfs defaults 0 0
/dev/disk/by-id/wwn-0x5000cca295caac7a-part1 /mnt/Pool/Disk5 xfs defaults 0 0
/dev/disk/by-id/wwn-0x5000cca2a1dcb6af-part1 /mnt/Pool/Disk6 xfs defaults 0 0
/dev/disk/by-id/wwn-0x50014ee20d104997-part1 /mnt/Pool/Disk7 xfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;MergerFS pool:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/mnt/Pool/Disk* /media/Storage fuse.mergerfs direct_io,defaults,allow_other,dropcacheonclose=true,inodecalc=path-hash,category.create=mfs,minfreespace=50G,fsname=storage 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;NFS exports in &lt;code&gt;/etc/exports&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/media/Storage/ 192.168.1.0/24(all_squash,anongid=1001,anonuid=1000,insecure,rw,subtree_check,fsid=0)
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;That&amp;rsquo;s it. No GUI. No abstraction layers. Linux doing what Linux does best: serving files reliably.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="common-mistakes-that-i-made-so-you-dont-have-to"&gt;Common Mistakes (That I Made So You Don&amp;rsquo;t Have To)
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Running storage as a VM on your compute host&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Race conditions, mount order chaos, reboots that break everything. This was my life for six months. Don&amp;rsquo;t do it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running storage in an LXC&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Even worse than a VM. Filesystem passthrough nightmares, permission issues, SMART monitoring doesn&amp;rsquo;t work right. Don&amp;rsquo;t.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Using USB externals without redundancy&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;One disconnect = data loss. If the data matters, it needs to be on real SATA connected to a real motherboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixing different drive sizes in a ZFS pool&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Kills performance, wastes capacity. ZFS wants matched vdevs. (MergerFS doesn&amp;rsquo;t care. One of many reasons I prefer it for media.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ignoring ECC RAM when building ZFS&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Silent bit flips can corrupt your &amp;ldquo;perfect&amp;rdquo; checksummed pool. If you go ZFS, get ECC. Or run MergerFS and skip the RAM requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never testing restores&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;You don&amp;rsquo;t have a backup until you&amp;rsquo;ve tested the restore. Same goes for RAID/parity rebuilds. Pull a drive while the system is OFF (don&amp;rsquo;t hot-swap unless you know what you&amp;rsquo;re doing) and verify you can identify and recover from the failure.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusting your setup without monitoring&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Run SMART checks monthly. Watch your logs. Drives give warnings before they die, if you&amp;rsquo;re paying attention.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thinking &amp;ldquo;I&amp;rsquo;ll add redundancy later&amp;rdquo;&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Later never comes. If the data matters NOW, protect it NOW. I&amp;rsquo;m guilty of this (took me 2 years to decide on SnapRAID), but at least I knew what I was risking.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-only-time-storage-in-a-vm-makes-sense"&gt;The Only Time Storage In A VM Makes Sense
&lt;/h2&gt;&lt;p&gt;Let me be fair: is there ANY scenario where running storage in a VM is acceptable?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maybe - MAYBE - if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re running TrueNAS as a VM with direct PCI passthrough of an HBA&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s THE ONLY VM on a dedicated Proxmox host (no competing VMs/LXCs)&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ve pinned the VM to specific CPU cores, so it always has resources&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ve configured Proxmox to boot the storage VM first with significant delays before anything else starts&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ve tested cold boot scenarios extensively&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;But even then:&lt;/strong&gt; what are you gaining? You&amp;rsquo;ve added a hypervisor layer between your storage and your compute. You&amp;rsquo;ve introduced complexity, potential for race conditions, and dependency on Proxmox functioning correctly.&lt;/p&gt;
&lt;p&gt;The question isn&amp;rsquo;t &amp;ldquo;can you make it work?&amp;rdquo; The question is &amp;ldquo;why bother?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My answer:&lt;/strong&gt; There is no good reason. The &amp;ldquo;one box does everything&amp;rdquo; dream is exactly that, a dream. You&amp;rsquo;re trading $50-100 of old hardware for hours of maintenance and fragility you don&amp;rsquo;t need.&lt;/p&gt;
&lt;p&gt;If you want to run TrueNAS or OpenMediaVault, run them on bare metal. If you want Debian + MergerFS, run it on bare metal. Give storage its own box and let it do its job without interference.&lt;/p&gt;
&lt;h2 id="what-you-should-do-right-now"&gt;What You Should Do Right Now
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re currently running storage in a VM or LXC:&lt;/p&gt;
&lt;h3 id="option-1-build-a-new-nas-box-recommended"&gt;Option 1: Build A New NAS Box (Recommended)
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Get cheap hardware&lt;/strong&gt;: Old desktop, $50-100 on Craigslist/eBay, needs 4+ SATA ports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install Debian&lt;/strong&gt; (or your preferred NAS OS, on bare metal)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up NFS/SMB shares&lt;/strong&gt; while your VM is still running&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copy data&lt;/strong&gt; from VM to new box (rsync, verify with diff/checksums)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test thoroughly&lt;/strong&gt; (mount from Proxmox, verify Jellyfin sees everything)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cut over&lt;/strong&gt;: Update Proxmox mounts to point to new NAS IP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shut down the VM forever&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Downtime: ~2 hours for final cutover. Peace of mind: priceless.&lt;/p&gt;
&lt;h3 id="option-2-repurpose-your-proxmox-host"&gt;Option 2: Repurpose Your Proxmox Host
&lt;/h3&gt;&lt;p&gt;If you only have one box:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Back up your containers/VMs&lt;/strong&gt; (you should be doing this anyway)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wipe Proxmox&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install Debian as NAS&lt;/strong&gt; on the bare metal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buy a second box&lt;/strong&gt; for Proxmox (can be cheaper since it&amp;rsquo;s compute only)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rebuild your compute stack&lt;/strong&gt; on the new Proxmox host&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mount from the NAS&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Yes, this requires buying hardware. But you&amp;rsquo;re separating concerns the right way. Storage on one box, compute on another. This is how it should be.&lt;/p&gt;
&lt;h3 id="option-3-stay-in-vm-hell"&gt;Option 3: Stay In VM Hell
&lt;/h3&gt;&lt;p&gt;Continue debugging mount failures, systemd dependencies, and race conditions. Spend 3 hours every few weeks troubleshooting why reboots break everything. Wonder why your spouse hates your &amp;ldquo;hobby.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t recommend this option.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;I run 93TB of media on bare metal Debian with MergerFS. Eight drives of different sizes, XFS file systems, pooled with MergerFS. No parity yet (adding SnapRAID next month). 2.5GbE network via 10GbE NIC to SODOLA switch. NFS shares to Proxmox. SMB shares to Windows clients.&lt;/p&gt;
&lt;p&gt;Hardware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel G3220 (old, low-power, sufficient)&lt;/li&gt;
&lt;li&gt;Gigabyte GA-Z87X-D3H motherboard&lt;/li&gt;
&lt;li&gt;16GB DDR3 RAM&lt;/li&gt;
&lt;li&gt;LSI HBA in IT mode (direct drive access, no RAID controller nonsense)&lt;/li&gt;
&lt;li&gt;Intel X520-DA1 10GbE NIC&lt;/li&gt;
&lt;li&gt;Rosewill Helium NAS case (holds 10 drives, quiet, fits under a desk)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The box boots in 90 seconds. Serves files to two Proxmox hosts and a dozen containers. I haven&amp;rsquo;t touched it in months except to check SMART data.&lt;/p&gt;
&lt;p&gt;Total cost: ~$2,000 over three years (includes drives, case, and network gear).&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t had a mount failure in six months. I reboot my compute nodes whenever I want. Proxmox updates don&amp;rsquo;t scare me. Power outages resolve themselves. My storage&amp;hellip; works.&lt;/p&gt;
&lt;p&gt;If your media library matters, and if you&amp;rsquo;re reading this, it does - give it its own box. Separate storage from compute. Pick an OS (I chose Debian + MergerFS, you might prefer OpenMediaVault or Unraid). Learn it. Build it right. Run it on bare metal.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll thank yourself the next time the power comes back and everything comes back online in the right order. Or better yet, you&amp;rsquo;ll sleep through it because there&amp;rsquo;s nothing to debug.&lt;/p&gt;
&lt;p&gt;Stop running storage in VMs. Stop fighting race conditions. Stop spending weekends debugging mount failures.&lt;/p&gt;
&lt;p&gt;Build it right. Run it on bare metal. Forget it exists.&lt;/p&gt;
&lt;h2 id="series-navigation"&gt;Series Navigation
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-hardware-guide-2025" &gt;Part 1 - The 3 Boxes That Run Your Media Server (2025 Edition)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-router-2025" &gt;Part 2 - Why Your ISP Router Is Killing Your Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025" &gt;Part 3 - Your Storage Deserves Its Own Box&lt;/a&gt; ← You are here&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-compute-2025" &gt;Part 4 - Proxmox Is Your New Best Friend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-backup-2025" &gt;Part 5 - Backups That Work (With PBS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Best Homelab Router: OPNsense Setup &amp; Hardware Picks</title><link>https://diymediaserver.com/post/media-server-router-2025/</link><pubDate>Thu, 30 Oct 2025 07:25:02 -0600</pubDate><guid>https://diymediaserver.com/post/media-server-router-2025/</guid><description>&lt;img src="https://diymediaserver.com/post/media-server-router-2025/featured_hu_75093ab916452dfb.webp" alt="Featured image of post Best Homelab Router: OPNsense Setup &amp; Hardware Picks" /&gt;&lt;p&gt;Consumer routers try to be everything: modem, router, switch, Wi-Fi, DNS, sometimes even a media server. When any piece fails, everything fails. The Wi-Fi radios cook themselves. Firmware updates brick the UI. NAT tables fill and kill your connections. Meanwhile, vulnerabilities sit unpatched for months.&lt;/p&gt;
&lt;p&gt;I learned this the hard way.&lt;/p&gt;
&lt;p&gt;My journey to a proper router started with a failed consumer device.
Then it evolved into &amp;ldquo;I can add a dual port NIC to the server and run my router in a VM&amp;rdquo;.
It started brilliantly, but it quickly turned into a twice-weekly nightmare.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Your ISP router can&amp;rsquo;t do VLANs, logging, or real security. A proper router box ($150-$800) running OPNsense gives you network segmentation, intrusion detection, full traffic visibility, and actual control. I&amp;rsquo;ve run this setup for 8+ years across multiple hardware generations. Setup time: one weekend to get working, 2-3 weekends to get truly secure.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-all-in-one-trap"&gt;The All-in-One Trap
&lt;/h2&gt;&lt;p&gt;I was running my entire homelab on a single Proxmox server: NAS, media server, download clients, home automation, everything. And because I was already virtualizing everything else, why not the router too?&lt;/p&gt;
&lt;p&gt;It seemed elegant. One box, total control, efficient resource usage.&lt;/p&gt;
&lt;p&gt;Then reality hit.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Why is the internet out again?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It was a Tuesday night. My wife was trying to stream something. I was in my office, staring at an LXC container that refused to stop, forcing me to reboot the entire Proxmox host. Again.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Sorry, it should be back on in a few minutes. I had to reboot the server again.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;This happened once or twice a week. Sometimes it was NFS issues that wouldn&amp;rsquo;t resolve without a full reboot. Sometimes hardware failures. Sometimes I was testing something and broke it in unexpected ways. Each time, the entire house lost internet.&lt;/p&gt;
&lt;p&gt;Five minutes of downtime doesn&amp;rsquo;t sound like much. But when it happens regularly, it stops being &amp;ldquo;a brief inconvenience&amp;rdquo; and starts being &amp;ldquo;why do we have internet problems all the time?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The breaking point came when a power supply died on a Friday morning. The internet was down for four hours while I left work to drive to Best Buy for a replacement part. Not ideal when your wife works from home and needs reliable connectivity.&lt;/p&gt;
&lt;h2 id="before-that-the-consumer-router-carousel"&gt;Before That: The Consumer Router Carousel
&lt;/h2&gt;&lt;p&gt;But let&amp;rsquo;s back up. Before I virtualized my router, I was doing what everyone does: buying consumer all-in-one routers and hoping they&amp;rsquo;d last.&lt;/p&gt;
&lt;p&gt;For about 15 years, I cycled through them. Linksys. Netgear Nighthawks. ASUS routers. It didn&amp;rsquo;t matter which brand. They all developed issues after 2-3 years. Random reboots. WAN ports that stopped working. WiFi that became progressively more unreliable until it wasn&amp;rsquo;t reliable at all.&lt;/p&gt;
&lt;p&gt;Toward the end of each router&amp;rsquo;s life, I&amp;rsquo;d be rebooting it daily to keep things working.&lt;/p&gt;
&lt;p&gt;The pattern was always the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Buy new router ($120-300)&lt;/li&gt;
&lt;li&gt;Works great for 18 months&lt;/li&gt;
&lt;li&gt;Starts having &amp;ldquo;quirks&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Quirks become daily problems&lt;/li&gt;
&lt;li&gt;Replace and repeat&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Over 15 years, I probably spent $1,500+ on routers that became e-waste. And that&amp;rsquo;s not counting the time spent troubleshooting, rebooting, and dealing with random failures.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-solution-separation"&gt;The Solution: Separation
&lt;/h2&gt;&lt;p&gt;The answer was obvious in hindsight: stop trying to make one device do everything.&lt;/p&gt;
&lt;p&gt;I started with an old computer running OPNsense. It wasn&amp;rsquo;t pretty, it wasn&amp;rsquo;t efficient, but it worked. More importantly, it &lt;strong&gt;kept working&lt;/strong&gt; when I rebooted my Proxmox host.&lt;/p&gt;
&lt;p&gt;I ran that setup for about six years. The old computer hummed along, doing its one job well, all while I learned what a proper router could do.&lt;/p&gt;
&lt;p&gt;About two years ago, I upgraded to a Protectli Vault Pro VP2420 for better power efficiency and added a &amp;ldquo;managed&amp;rdquo; switch (SODOLA 8-Port 2.5Gb). The principle remained the same, only more refined.&lt;/p&gt;
&lt;p&gt;Give each job a proper box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Router:&lt;/strong&gt; Dedicated hardware running OPNsense&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; Separate NAS that stays up 24/7&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute:&lt;/strong&gt; Proxmox host for everything else&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll never forget the first time I rebooted my compute node after the separation. I hit the command, waited for the host to go down, and then&amp;hellip; nothing happened. The internet stayed up. My wife kept streaming. Nobody called my name.&lt;/p&gt;
&lt;p&gt;It was amazing.&lt;/p&gt;
&lt;h2 id="what-changed-when-i-switched"&gt;What Changed When I Switched
&lt;/h2&gt;&lt;h3 id="before-consumervirtualized-router"&gt;Before (Consumer/Virtualized Router):
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Visibility:&lt;/strong&gt; I could see connected devices and some basic stats. That&amp;rsquo;s it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; A checkbox labeled &amp;ldquo;firewall enabled.&amp;rdquo; No logs, no alerts, no real control. When I checked my Jellyfin logs one day, I discovered someone in Romania had been hammering my server, trying to brute force the default username. My old router never told me this was happening.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; 4K streaming while SABnzbd maxed out my download connection meant random buffering and frustrated family members.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Segmentation:&lt;/strong&gt; Everything on one network. My Jellyfin server could see my IoT lightbulbs. My download containers could access my NAS directly. Zero isolation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uptime:&lt;/strong&gt; Hostage to whatever I was doing on my Proxmox host. Every experiment, every update, every hardware issue meant the whole house went offline.&lt;/p&gt;
&lt;h3 id="after-dedicated-opnsense-box"&gt;After (Dedicated OPNsense Box):
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Visibility:&lt;/strong&gt; Every connection, every blocked attempt, every DNS query. I see exactly what my network is doing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Five VLANs with explicit allow rules. IDS/IPS running Suricata with Emerging Threats rules. I see attempted port scans and exploit attempts weekly that my old setup never noticed. That Romanian brute force attempt? Now I&amp;rsquo;d see it in real-time and could block the entire country if needed. (All other countries are now blocked from everything I&amp;rsquo;m running)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; QoS rules mean Jellyfin never stutters, even when SABnzbd maxes out the connection. Downloads automatically throttle when someone starts streaming. No manual intervention needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Segmentation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10.0.1.x: Trusted devices (laptop, phone)&lt;/li&gt;
&lt;li&gt;10.0.2.x: Media stack (Jellyfin, Sonarr, Radarr)&lt;/li&gt;
&lt;li&gt;10.0.3.x: IoT devices (cameras, lights)&lt;/li&gt;
&lt;li&gt;10.0.4.x: Guest WiFi (isolated)&lt;/li&gt;
&lt;li&gt;10.0.5.x: Management interfaces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My IoT devices literally cannot see my NAS. The firewall says &amp;ldquo;Hell no&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Uptime:&lt;/strong&gt; Router runs independently. I can reboot, upgrade, or completely rebuild my compute node without anyone noticing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The difference is night and day. I went from hoping my network was secure to &lt;em&gt;knowing&lt;/em&gt; it is. I went from apologizing for internet outages to working on my servers whenever I need to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-your-current-router-is-failing-you"&gt;Why Your Current Router is Failing You
&lt;/h2&gt;&lt;h3 id="what-advanced-security-means"&gt;What &amp;ldquo;Advanced Security&amp;rdquo; Means
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Consumer router claims:&lt;/strong&gt; &amp;ldquo;Advanced Security Features!&amp;rdquo;
&lt;strong&gt;Reality:&lt;/strong&gt; Scanning for obvious malware from 2015. Maybe.&lt;/p&gt;
&lt;p&gt;Real security means IDS/IPS with updated rulesets, traffic analysis, and actual logging. Your $120 router isn&amp;rsquo;t doing that.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t realize how blind I was until I enabled Suricata on OPNsense. Every week I see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Port scans from random IPs&lt;/li&gt;
&lt;li&gt;Exploit attempts targeting common vulnerabilities&lt;/li&gt;
&lt;li&gt;IoT devices calling home to suspicious destinations&lt;/li&gt;
&lt;li&gt;Brute force attempts on exposed services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My consumer router saw none of this. It was all happening, every day, and I had no idea.&lt;/p&gt;
&lt;h3 id="the-single-point-of-failure-problem"&gt;The Single Point of Failure Problem
&lt;/h3&gt;&lt;p&gt;Your consumer router tries to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router + Firewall + WiFi + Switch + DHCP + DNS + VPN + USB file server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When any piece fails, everything fails. When you need to reboot, everything goes down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A proper network has separate components:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Router box&lt;/strong&gt;: Routing, firewall, DHCP, DNS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Switch&lt;/strong&gt;: Distributes connections, handles VLANs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access points&lt;/strong&gt;: WiFi only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the AP reboots, your wired devices keep working. When you&amp;rsquo;re tinkering with your Proxmox host, your family doesn&amp;rsquo;t notice.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This isn&amp;rsquo;t theoretical. After eight years of running separated infrastructure, I&amp;rsquo;ve had zero network-wide outages that weren&amp;rsquo;t ISP-related. With consumer routers, I had them weekly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="what-youre-exposing"&gt;What You&amp;rsquo;re Exposing
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Without proper segmentation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IoT device gets compromised&lt;/strong&gt; → Attacker accesses your entire network
&lt;strong&gt;With VLANs:&lt;/strong&gt; They&amp;rsquo;re trapped in the IoT sandbox&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Download client compromised&lt;/strong&gt; → Direct path to all your files
&lt;strong&gt;With VLANs:&lt;/strong&gt; Container can only talk to specific services&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Smart device has vulnerability&lt;/strong&gt; → Entry point to everything
&lt;strong&gt;With VLANs:&lt;/strong&gt; Isolated and contained&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Real example: I tried to tighten security rules on my IoT VLAN once. Made them too restrictive and all my devices lost internet access. No problem. I restored my OPNsense backup from 15 minutes earlier and everything was fine. Try doing that with a consumer router.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-features-that-matter"&gt;The Features That Matter
&lt;/h2&gt;&lt;h3 id="1-vlan-support-network-segmentation"&gt;1. VLAN Support (Network Segmentation)
&lt;/h3&gt;&lt;p&gt;Create multiple isolated networks on the same hardware. If one thing gets compromised, it can&amp;rsquo;t spread.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real example:&lt;/strong&gt; My media stack runs in VLAN 2 (10.0.2.x). Firewall rules allow internet access for downloading and trusted device access for streaming. Everything else is denied. If a container gets compromised, the damage is contained to that VLAN.&lt;/p&gt;
&lt;p&gt;I thought about my VLAN structure for a long time before implementing it. Two years later, I wouldn&amp;rsquo;t change anything about how it&amp;rsquo;s set up.&lt;/p&gt;
&lt;h3 id="2-intrusion-detectionprevention-idsips"&gt;2. Intrusion Detection/Prevention (IDS/IPS)
&lt;/h3&gt;&lt;p&gt;Your router inspects every packet for known attacks, malware, and suspicious behavior.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t enable Suricata from day one. I added it after getting comfortable with the basics. The learning curve was real. Lots of forum posts and Reddit threads to understand what I was seeing. But it wasn&amp;rsquo;t too bad, and now it runs automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I see every week:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blocked port scans from random IPs&lt;/li&gt;
&lt;li&gt;Exploit attempts targeting known vulnerabilities&lt;/li&gt;
&lt;li&gt;Malware callbacks from IoT devices&lt;/li&gt;
&lt;li&gt;Geographic attacks I can block at the firewall level&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My old consumer router saw none of this. It was all happening, I didn&amp;rsquo;t know about it.&lt;/p&gt;
&lt;h3 id="3-real-firewall-rules--full-logging"&gt;3. Real Firewall Rules + Full Logging
&lt;/h3&gt;&lt;p&gt;Define exactly what&amp;rsquo;s allowed. See every decision the firewall makes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My media VLAN rules:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Allow: Internet access (ports 80, 443)&lt;/li&gt;
&lt;li&gt;Allow: Internal service communication&lt;/li&gt;
&lt;li&gt;Allow: Trusted devices → media services&lt;/li&gt;
&lt;li&gt;Allow: NAS access (NFS)&lt;/li&gt;
&lt;li&gt;Deny: Everything else (log it)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When something breaks, I check the logs and see exactly what&amp;rsquo;s being denied. No guessing. No mystery reboots. Only clear information about what happened and why.&lt;/p&gt;
&lt;h3 id="4-local-dns--dhcp-control"&gt;4. Local DNS + DHCP Control
&lt;/h3&gt;&lt;p&gt;Static IPs for all services. Custom DNS entries. Full control.&lt;/p&gt;
&lt;p&gt;Instead of remembering 10.0.2.15:8096, I go to &lt;code&gt;jellyfin.home&lt;/code&gt;. All my services have friendly names. Static DHCP means each device always gets the same IP every time. This is critical for firewall rules that actually work.&lt;/p&gt;
&lt;h3 id="5-traffic-shaping--qos"&gt;5. Traffic Shaping + QoS
&lt;/h3&gt;&lt;p&gt;Prioritize certain traffic. Set bandwidth limits per device.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t set this up immediately. I added QoS after SABnzbd kept maxing out my 150Mbps download connection and causing buffering on Jellyfin streams.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My current setup (tailored to my needs):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Priority 1: Streaming (Jellyfin, Plex)&lt;/li&gt;
&lt;li&gt;Priority 2: YouTube and video&lt;/li&gt;
&lt;li&gt;Priority 3: Gaming&lt;/li&gt;
&lt;li&gt;Priority 4: Web browsing&lt;/li&gt;
&lt;li&gt;Priority 5: Bulk downloads (capped at 80% of available bandwidth)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now downloads automatically throttle when someone starts streaming. No buffering, no complaints, no manual intervention.&lt;/p&gt;
&lt;h2 id="router-hardware-options"&gt;Router Hardware Options
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Note on recommendations:&lt;/strong&gt; I&amp;rsquo;ve personally used all the hardware in the Budget and Balanced builds over the years. The Beast build represents what I&amp;rsquo;d upgrade to if I had the time and budget. It is based on research and community feedback, not hands-on experience.&lt;/p&gt;
&lt;h3 id="budget-build-200-total"&gt;Budget Build (~$200 Total)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Router Box: $80-170&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used off-lease SFF computers are the best value for starting out. I ran OPNsense on an old desktop computer for six years before upgrading.&lt;/p&gt;
&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/dell_wyse_5070_hu_48b8240817f351c5.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/dell_wyse_5070_hu_d039890e1b74ebb8.webp" width="600" height="632" alt="Dell Wyse 5070 Extended (J5005)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Dell Wyse 5070 Extended (J5005)&lt;/strong&gt;
This is the budget-friendly, low-power OPNsense box that works out of the box. Add an Intel i350-T2, give it 8–16 GB of RAM, and you&amp;rsquo;ve got a quiet, reliable home router with room for VLANs, WireGuard, and moderate IDS.
&lt;strong&gt;Price&lt;/strong&gt;: $80 - $170
&lt;strong&gt;Personal note&lt;/strong&gt;: This is comparable to what I started with. Old hardware running OPNsense for years without issues.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=Dell&amp;#43;Wyse&amp;#43;5070&amp;#43;Extended&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Network Card: $30-50&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used Intel NICs are reliable and well-supported in OPNsense and PFsense.&lt;/p&gt;
&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_i350-T2_hu_dcf91dbbd050f00c.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_i350-T2_hu_df229c95abd32869.webp" width="600" height="450" alt="Intel i350-T2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel i350-T2&lt;/strong&gt;
The low-profile Intel i350-T2 drops right into the Wyse 5070 Extended&amp;rsquo;s PCIe slot and gives you two rock-solid 1 GbE ports with mature FreeBSD drivers, perfect for OPNsense.
&lt;strong&gt;Price&lt;/strong&gt;: $30 - $50
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=Intel&amp;#43;i350-T2&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Switch: $60-90&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You need a managed switch to handle VLANs properly.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07GRG63P6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/tp-link-16port_hu_440a6797edcacded.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/tp-link-16port_hu_77586bf5c752fc13.webp" width="600" height="460" alt="TP-Link TL-SG116E 16-Port Easy Smart Switch" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link TL-SG116E&lt;/strong&gt;
Is a 16-port, fanless Gigabit &amp;ldquo;Easy Smart&amp;rdquo; switch with a metal shell, web GUI, and essentials like 802.1Q VLANs, trunks, and QoS. It&amp;rsquo;s a great budget match for an OPNsense router build: quiet, low-power, cheap, and gives you the VLAN segmentation and uplink tagging you need without paying managed-switch tax.
&lt;strong&gt;Personal note&lt;/strong&gt;: I used this class of switch for years before upgrading to 2.5GbE.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JKmBR4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Dedicated Wireless Access Point (WAP): $60-80&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Having a dedicated WAP can really improve the Wi-Fi signal in your house. You can place it in the optimal location with only an ethernet cable.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0781YXFBT"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/tp-link-eap225_hu_da99831971d7e824.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/tp-link-eap225_hu_bb15f439bac0ed47.webp" width="600" height="574" alt="TP-Link EAP225 Omada AC1350 Ceiling-Mount AP" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link EAP225&lt;/strong&gt;
Is a ceiling-mount, PoE-powered, dual-band 802.11ac with a Gigabit uplink, and simple Omada controller management. It honors 802.1Q VLANs for clean SSID-to-network mapping (Main/IoT/Guest), and delivers fast, reliable Wi-Fi.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47pN6Ey" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What this setup can handle:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gigabit routing with IDS/IPS&lt;/li&gt;
&lt;li&gt;5-6 VLANs&lt;/li&gt;
&lt;li&gt;Up to 50 devices&lt;/li&gt;
&lt;li&gt;Basic VPN&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; 95% of home users with gigabit internet or slower.&lt;/p&gt;
&lt;h3 id="balanced-build-500-total"&gt;Balanced Build (~$500 Total)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;This is what I currently run.&lt;/strong&gt; After six years on budget hardware, I upgraded to this configuration two years ago for better power efficiency and multi-gig speeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Router Box: $320-500&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Protectli Vault Pro VP2420-4 Port&lt;/strong&gt; is a fanless mini-appliance with a 4-core Intel J6412, 4× 2.5GbE Intel i225 ports, and AES-NI. It&amp;rsquo;s a strong mid-tier OPNsense base that stays quiet and sips power while handling gigabit-plus routing, as well as fast VPN access.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Personal note&lt;/strong&gt;: This is exactly what I&amp;rsquo;m running. Two years in, zero hardware issues. It&amp;rsquo;s silent, efficient, and handles everything I throw at it. It has since been discontinued, so there&amp;rsquo;s no buy link for it any more. Anything fanless with four 2.5GbE Intel NICs and AES-NI lands in the same place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Switch: $90&lt;/strong&gt;&lt;/p&gt;
&lt;div class="product-box" data-asin="B0GKPNRFGF"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-8port_hu_ffc7d51ed426b5ba.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-8port_hu_512d55343887425b.webp" width="600" height="600" alt="TP-Link Omada ES210X-M2 8-Port 2.5GbE Switch" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada ES210X-M2 8-Port 2.5GbE Switch&lt;/strong&gt;
Provides eight 2.5GbE ports and two 10G SFP+ uplinks in a quiet fanless metal chassis, with an Easy Managed web UI covering 802.1Q VLANs, IGMP snooping, and auto loop prevention. It fits a mid-tier router build by unlocking multi-gig LAN speeds for NAS and desktops while keeping segmentation clean and power draw low.
&lt;strong&gt;Personal note&lt;/strong&gt;: The switch I actually installed here was the SODOLA 8-Port, added when I upgraded to the Protectli. It&amp;rsquo;s since been discontinued, so the Omada above is the equivalent I&amp;rsquo;d buy today. VLANs were straightforward to set up on the SODOLA, and the move to 2.5GbE made a real difference for NAS transfers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B07USpZzq" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Wireless Access Points&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I run four TP-Link Deco units in a mesh configuration. Mostly I use them as dumb APs, but the controller software is useful for one thing: forcing specific devices to specific APs. Some devices are too aggressive about switching APs, which caused buffering during video playback. Pinning them to one AP solved it.&lt;/p&gt;
&lt;p&gt;For new buyers, I&amp;rsquo;d recommend proper ceiling-mount APs instead:&lt;/p&gt;
&lt;div class="product-box" data-asin="B0FKRF82DX"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-dual-band-7_hu_ce304ea7fa95c4b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-dual-band-7_hu_6618450a0acf8e8e.webp" width="600" height="602" alt="TP-Link Omada WiFi 7 AP (Dual-Band, 2.5GbE)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada WiFi 7 - Dual Band&lt;/strong&gt;
A PoE ceiling-mount WiFi 7 access point with a 2.5 GbE uplink, Omada controller support, and VLAN-aware SSIDs for clean network segmentation. It fits a mid-tier router build by delivering fast, reliable wireless, simple central management, and a clear upgrade path without blowing the budget.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47x7SCk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What this setup can handle:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-gig speeds (2.5GbE)&lt;/li&gt;
&lt;li&gt;Heavy IDS/IPS&lt;/li&gt;
&lt;li&gt;VPN at line speed&lt;/li&gt;
&lt;li&gt;50+ devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; People who want room to grow and don&amp;rsquo;t want to upgrade again in 2-3 years.&lt;/p&gt;
&lt;h3 id="beast-build-1200-total"&gt;Beast Build (~$1,200 Total)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Full disclosure:&lt;/strong&gt; I haven&amp;rsquo;t personally used this hardware. This represents what I&amp;rsquo;d upgrade to if I had the budget and needed more performance. Based on community feedback and research, not hands-on experience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Router Box: $600-700&lt;/strong&gt;&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DXNFP13J"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ms-01_hu_ab59b734919ebff7.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ms-01_hu_828a8b73ed5a92f5.webp" width="600" height="265" alt="MINISFORUM MS-01 Mini Workstation (i5-12600H, 32GB &amp;#43; 1TB)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-01 Mini Workstation&lt;/strong&gt;
A compact workstation with a high-core-count Intel CPU, dual 10 GbE SFP+ ports, dual 2.5 GbE, NVMe storage, and room to scale. It is ideal for a top-tier OPNsense build because it can push multi-gig routing, run IDS/IPS and high-speed VPN without breaking a sweat and still leave room for future upgrades.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/493Htx3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Switch: $360&lt;/strong&gt;&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DLGTC15B"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/sodola-24-port-poe_hu_987d0056a1997baf.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/sodola-24-port-poe_hu_e0bea2fc5a48ea32.webp" width="600" height="600" alt="SODOLA 24-Port 2.5GbE PoE Managed Switch" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;SODOLA 24 Port 2.5Gb PoE Switch&lt;/strong&gt;
This switch packs dense multi-gig PoE for APs, cameras, and IoT while keeping management simple with VLANs, and QoS. It&amp;rsquo;s a strong top-tier fit because your OPNsense box can push multi-gig uplinks and this switch distributes that speed across the whole network without noisy rack gear.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3XdIKdE" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Access Point: $200&lt;/strong&gt;&lt;/p&gt;
&lt;div class="product-box" data-asin="B0FJQL2T2V"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/omada-dual-band-7_hu_ce304ea7fa95c4b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/omada-dual-band-7_hu_6618450a0acf8e8e.webp" width="600" height="602" alt="TP-Link Omada WiFi 7 AP (Tri-Band, EAP773)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada WiFi 7 - Tri-Band&lt;/strong&gt;
Brings 2.4/5/6 GHz and Multi-Link Operation for large capacity, and low latency. It fits a top-tier OPNsense build with a multi-gig PoE uplink, clean SSID-to-VLAN mapping, Omada controller features like fast roaming and band steering, and headroom for dense homes or busy offices.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/47x7vHW" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What this setup can handle:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10GbE at line speed&lt;/li&gt;
&lt;li&gt;Extremely complex rules&lt;/li&gt;
&lt;li&gt;Multiple simultaneous VPN connections&lt;/li&gt;
&lt;li&gt;100+ devices&lt;/li&gt;
&lt;li&gt;Future-proof for years&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="choosing-an-access-point"&gt;Choosing an Access Point
&lt;/h3&gt;&lt;p&gt;Whatever build you land on, the access point is what makes or breaks wireless VLAN support: it broadcasts your SSIDs and tags each one to a VLAN before handing frames to the switch. A controller-based AP (TP-Link Omada or UniFi) does this cleanly. A plain consumer AP, or a mesh kit that can&amp;rsquo;t tag per SSID, dumps every client into one VLAN and defeats the whole point. I run Omada for the cost-to-feature ratio, with my old Deco mesh units demoted to dumb APs.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
For the full wireless build, covering the best homelab AP, mapping SSIDs to VLANs, AP placement, and troubleshooting, see the dedicated guide: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/2026/proxmox-vlan-wifi-homelab/" &gt;Best Homelab Wi-Fi &amp;amp; Access Points: VLAN-Aware Setup&lt;/a&gt;&lt;/strong&gt;.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="opnsense-vs-pfsense-which-should-you-choose"&gt;OPNsense vs pfSense: Which Should You Choose?
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Choose OPNsense if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re new and want a cleaner interface&lt;/li&gt;
&lt;li&gt;You want built-in IDS/IPS&lt;/li&gt;
&lt;li&gt;You prefer frequent updates&lt;/li&gt;
&lt;li&gt;All features are included without licensing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Choose pfSense if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want the most mature platform&lt;/li&gt;
&lt;li&gt;You prioritize stability over features&lt;/li&gt;
&lt;li&gt;You need the largest community&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t mind a more corporate feel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;My recommendation:&lt;/strong&gt; Start with OPNsense. The interface is friendlier while remaining equally powerful. I&amp;rsquo;ve run it for eight years across multiple hardware generations without major issues.&lt;/p&gt;
&lt;p&gt;Either choice is infinitely better than a consumer router.&lt;/p&gt;
&lt;h2 id="your-first-weekend-with-opnsense"&gt;Your First Weekend with OPNsense
&lt;/h2&gt;&lt;p&gt;Let me be honest about timelines. My original draft said &amp;ldquo;48 hours&amp;rdquo; but that&amp;rsquo;s optimistic. Here&amp;rsquo;s what actually happened when I set mine up.&lt;/p&gt;
&lt;h3 id="hour-1-2-basic-installation"&gt;Hour 1-2: Basic Installation
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download OPNsense ISO, flash to USB&lt;/li&gt;
&lt;li&gt;Boot router box, install to internal drive&lt;/li&gt;
&lt;li&gt;Reboot, remove USB&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This part is straightforward. If you&amp;rsquo;ve installed Linux before, this will feel familiar.&lt;/p&gt;
&lt;h3 id="hour-2-4-initial-configuration"&gt;Hour 2-4: Initial Configuration
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Getting Internet Working:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect WAN to modem, LAN to computer&lt;/li&gt;
&lt;li&gt;Access web interface at 192.168.1.1&lt;/li&gt;
&lt;li&gt;Run setup wizard: Set password, configure WAN/LAN, set timezone&lt;/li&gt;
&lt;li&gt;Update to latest version&lt;/li&gt;
&lt;li&gt;Configure DNS and test internet&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Expect this to take longer than you think. My first attempt, I had to reboot the modem twice before the WAN interface pulled an IP. Normal stuff, but it eats time.&lt;/p&gt;
&lt;h3 id="weekend-2-3-vlans"&gt;Weekend 2-3: VLANs
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Creating Your First VLAN:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;VLANs are where it gets tricky. I spent time wrapping my head around the concept before implementation paid off.&lt;/p&gt;
&lt;p&gt;Start with IoT if you have IoT devices (and at this point most of us do). It&amp;rsquo;s the easiest to isolate and test:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create VLAN on your switch (VLAN ID: 3)&lt;/li&gt;
&lt;li&gt;Create VLAN interface in OPNsense (Interfaces → VLAN)&lt;/li&gt;
&lt;li&gt;Assign and configure the interface (10.0.3.1/24)&lt;/li&gt;
&lt;li&gt;Enable DHCP for the VLAN&lt;/li&gt;
&lt;li&gt;Create firewall rules:
&lt;ul&gt;
&lt;li&gt;Allow: IoT → Internet (ports 80, 443, 53)&lt;/li&gt;
&lt;li&gt;Allow: IoT → DNS Access to your DNS server&lt;/li&gt;
&lt;li&gt;Block: IoT → All other RFC1918 private networks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Test: Connect device, verify it works but can&amp;rsquo;t reach other VLANs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first VLAN took me several hours. The second took one hour. By the fifth, it was fifteen minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Common VLAN Mistakes I Made:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forgetting to tag the trunk port on the switch&lt;/strong&gt; - Spent 30 minutes troubleshooting why devices couldn&amp;rsquo;t get IPs before realizing the switch wasn&amp;rsquo;t passing VLAN tags.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Creating rules on the wrong interface&lt;/strong&gt; - Made perfect firewall rules on the WAN interface instead of the VLAN interface. They did nothing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Blocking DNS accidentally&lt;/strong&gt; - Blocked RFC1918 addresses but forgot that my router (which provides DNS) is on 10.0.1.1. IoT devices couldn&amp;rsquo;t resolve anything.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="week-3-4-idsips"&gt;Week 3-4: IDS/IPS
&lt;/h3&gt;&lt;p&gt;I didn&amp;rsquo;t enable Suricata immediately. Get comfortable with basic routing and VLANs first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When I added IDS/IPS:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Services → Intrusion Detection → Administration&lt;/li&gt;
&lt;li&gt;Enable IDS&lt;/li&gt;
&lt;li&gt;Download Emerging Threats ruleset&lt;/li&gt;
&lt;li&gt;Start in IDS mode (alerts only, no blocking)&lt;/li&gt;
&lt;li&gt;Watch alerts for a week&lt;/li&gt;
&lt;li&gt;Tune rules to reduce false positives&lt;/li&gt;
&lt;li&gt;Switch to IPS mode (active blocking)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The learning curve was real. Lots of alerts I didn&amp;rsquo;t understand. Lots of forum threads explaining what &amp;ldquo;ET POLICY&amp;rdquo; rules meant. But after a few weeks, it clicked.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Start with IDS mode only. Don&amp;rsquo;t enable blocking until you understand what you&amp;rsquo;re seeing. I almost blocked my entire media VLAN because I didn&amp;rsquo;t understand a legitimate traffic pattern Suricata flagged.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="week-4-6-how-to-configure-qos-in-a-homelab-network"&gt;Week 4-6: How to Configure QoS in a Homelab Network
&lt;/h3&gt;&lt;p&gt;QoS (Quality of Service) on a homelab network is traffic shaping on your router: you create pipes for your up/down bandwidth, sort traffic into priority queues, and let the firewall throttle bulk transfers when something more important needs the line. I added QoS when SABnzbd kept ruining movie night.&lt;/p&gt;
&lt;p&gt;Downloads would max out my 150Mbps connection, and Jellyfin streams would buffer. Annoying for me, unacceptable for my wife.&lt;/p&gt;
&lt;p&gt;Setting up traffic shaping:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Firewall → Shaper → Settings&lt;/li&gt;
&lt;li&gt;Create pipes for download/upload&lt;/li&gt;
&lt;li&gt;Create queues for different traffic types&lt;/li&gt;
&lt;li&gt;Assign priorities&lt;/li&gt;
&lt;li&gt;Test with actual usage&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My current priorities (tailored to 150/30 Mbps connection):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Queue 1 (Highest):&lt;/strong&gt; Jellyfin streaming, Plex&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue 2:&lt;/strong&gt; YouTube, video platforms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue 3:&lt;/strong&gt; Gaming traffic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue 4:&lt;/strong&gt; Web browsing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue 5 (Lowest):&lt;/strong&gt; SABnzbd, torrents (capped at 120Mbps / 80%)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now downloads run in the background without impacting anything else. No manual throttling needed.&lt;/p&gt;
&lt;h3 id="current-state-set-and-forget"&gt;Current State: Set and Forget
&lt;/h3&gt;&lt;p&gt;After the initial setup period, I barely touch OPNsense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weekly routine:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check for updates (takes 2 minutes)&lt;/li&gt;
&lt;li&gt;Review IDS/IPS alerts (5 minutes)&lt;/li&gt;
&lt;li&gt;Check logs if something seems weird (rarely needed)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Monthly:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Review firewall rules to see if anything needs adjustment (usually doesn&amp;rsquo;t)&lt;/li&gt;
&lt;li&gt;Download config backup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s it. The router runs without intervention. No daily reboots. No random failures. No &amp;ldquo;why is the internet down?&amp;rdquo; conversations.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In two years with the Protectli, I&amp;rsquo;ve had zero hardware failures. Compare that to 15 years of consumer routers dying every 2-3 years.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="common-problems-and-fixes"&gt;Common Problems and Fixes
&lt;/h2&gt;&lt;h3 id="cant-access-internet-after-setup"&gt;Can&amp;rsquo;t Access Internet After Setup
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; OPNsense installed, but no internet access&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WAN interface has IP address (Interfaces → Overview)&lt;/li&gt;
&lt;li&gt;Gateway shows as online (System → Gateways → Single)&lt;/li&gt;
&lt;li&gt;DNS servers configured (System → Settings → General)&lt;/li&gt;
&lt;li&gt;Try pinging 8.8.8.8 from Diagnostics → Ping&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Usually the modem needs a reboot. Power cycle it, wait 2 minutes, check again.&lt;/p&gt;
&lt;h3 id="vlans-not-working"&gt;VLANs Not Working
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Device on VLAN can&amp;rsquo;t get IP or access internet&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VLAN created on switch with correct ID&lt;/li&gt;
&lt;li&gt;Trunk port configured between router and switch&lt;/li&gt;
&lt;li&gt;VLAN interface assigned in OPNsense&lt;/li&gt;
&lt;li&gt;DHCP enabled for VLAN&lt;/li&gt;
&lt;li&gt;Firewall rules allow traffic&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; 90% of the time it&amp;rsquo;s switch configuration. Verify VLAN tagging is correct.&lt;/p&gt;
&lt;h3 id="locked-yourself-out"&gt;Locked Yourself Out
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Changed firewall rules, now can&amp;rsquo;t access web interface&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect directly to LAN port&lt;/li&gt;
&lt;li&gt;Access 192.168.1.1 (default LAN IP)&lt;/li&gt;
&lt;li&gt;If that doesn&amp;rsquo;t work: Boot to recovery mode, reset to last config&lt;/li&gt;
&lt;li&gt;Last resort: Reinstall and restore from backup (you made backups, right?)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why you download config backups regularly.&lt;/p&gt;
&lt;h3 id="specific-mistake-i-made-iot-security-lockdown"&gt;Specific Mistake I Made: IoT Security Lockdown
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;What I tried:&lt;/strong&gt; Tightened security rules on IoT VLAN to block more traffic&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt; All IoT devices lost internet access completely&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; I was too aggressive blocking outbound traffic. Smart devices need to call home for updates and cloud features. You can restrict them, but not completely isolate them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I recovered:&lt;/strong&gt; Restored OPNsense backup from 15 minutes earlier. Back to working state in 2 minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Always download a config backup before making major changes. Test changes incrementally. Have a rollback plan.&lt;/p&gt;
&lt;h2 id="common-mistakes-to-avoid"&gt;Common Mistakes to Avoid
&lt;/h2&gt;&lt;h3 id="over-complicating-day-one"&gt;Over-Complicating Day One
&lt;/h3&gt;&lt;p&gt;Start simple. Get basic routing working before adding anything else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My recommended progression:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Weekend 1: Install OPNsense, get internet working&lt;/li&gt;
&lt;li&gt;Weekend 2-3: Add one VLAN, test thoroughly&lt;/li&gt;
&lt;li&gt;Weekend 4: Add remaining VLANs&lt;/li&gt;
&lt;li&gt;Week 3-4: Enable IDS/IPS in monitoring mode&lt;/li&gt;
&lt;li&gt;Week 5-6: Add QoS if needed&lt;/li&gt;
&lt;li&gt;Ongoing: Tune and optimize&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Don&amp;rsquo;t try to do everything at once. I thought about my VLAN structure for weeks before implementing it. That planning paid off, I haven&amp;rsquo;t changed anything about how VLANs are organized in two years.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="not-documenting"&gt;Not Documenting
&lt;/h3&gt;&lt;p&gt;Keep notes on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What each VLAN is for&lt;/li&gt;
&lt;li&gt;Why specific firewall rules exist&lt;/li&gt;
&lt;li&gt;What each static IP is assigned to&lt;/li&gt;
&lt;li&gt;Changes you make and why&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Future you will thank you. I have a simple Obsidian note:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;VLAN 2 (10.0.2.x) - Media Stack
- Jellyfin, Sonarr, Radarr, SABnzbd
- Allows: Internet (80, 443), NAS access, Trusted devices
- Blocks: IoT VLAN, Guest VLAN
Rule: Allow trusted → Media (8096)
Why: Access Jellyfin from phones/laptops
Created: 2023-02-15
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple, but invaluable when troubleshooting.&lt;/p&gt;
&lt;h3 id="not-testing-firewall-rules"&gt;Not Testing Firewall Rules
&lt;/h3&gt;&lt;p&gt;After creating block rules, actually test them. Don&amp;rsquo;t assume they work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I test:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create rule to block IoT → NAS&lt;/li&gt;
&lt;li&gt;From IoT device, try to ping NAS IP&lt;/li&gt;
&lt;li&gt;Check firewall logs to confirm block&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the rule doesn&amp;rsquo;t show up in logs, it&amp;rsquo;s not working. Figure it out and fix it now, not when you have a security incident.&lt;/p&gt;
&lt;h3 id="no-config-backups"&gt;No Config Backups
&lt;/h3&gt;&lt;p&gt;Download your config regularly. Hardware can fail. Updates can go wrong. Accidents happen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My backup routine:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After any major change: Download config immediately&lt;/li&gt;
&lt;li&gt;Weekly: Download fresh backup (takes 30 seconds)&lt;/li&gt;
&lt;li&gt;Store backups in three places: NAS, cloud, USB drive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ve restored from backup twice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When I locked myself out with aggressive firewall rules&lt;/li&gt;
&lt;li&gt;When I tried an OPNsense update that didn&amp;rsquo;t play nice with my hardware (rolled back, waited for next version)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both times, I was back up in minutes instead of hours of reconfiguration.&lt;/p&gt;
&lt;h3 id="ignoring-idsips-alerts"&gt;Ignoring IDS/IPS Alerts
&lt;/h3&gt;&lt;p&gt;Review alerts weekly. This is your early warning system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I check every week:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New alert types I haven&amp;rsquo;t seen before&lt;/li&gt;
&lt;li&gt;Repeated alerts from same source (might need blocking)&lt;/li&gt;
&lt;li&gt;Internal devices generating alerts (compromised?)&lt;/li&gt;
&lt;li&gt;Geographic patterns (lots of traffic from one country?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Takes 5 minutes.&lt;/p&gt;
&lt;h2 id="integration-with-your-four-box-stack"&gt;Integration with Your Four-Box Stack
&lt;/h2&gt;&lt;p&gt;This is why we separate the router from everything else.&lt;/p&gt;
&lt;h3 id="network-topology"&gt;Network Topology
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;[Modem] → [Router/Firewall] → [Managed Switch]
├→ [NAS] (VLAN 2)
├→ [Compute Box] (VLAN 2)
├→ [Access Points] (Multiple VLANs)
└→ [Other Devices] (Various VLANs)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="boot-order-matters"&gt;Boot Order Matters
&lt;/h3&gt;&lt;p&gt;Router → Storage → Compute&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this order:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router boots first: Network is available for everything else&lt;/li&gt;
&lt;li&gt;Storage boots second: NAS is ready when compute needs it&lt;/li&gt;
&lt;li&gt;Compute boots last: Services start when dependencies are ready&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How to set delays:&lt;/strong&gt;
You might be able to set delays in BIOS/UEFI for each box depending on your hardware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Router: Auto-start immediately (0 second delay)&lt;/li&gt;
&lt;li&gt;Storage: 30-second delay after power on&lt;/li&gt;
&lt;li&gt;Compute: 60-second delay after power on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Real-world benefit:&lt;/strong&gt; Power outage happens. Everything comes back automatically in the right order. No manual intervention needed.&lt;/p&gt;
&lt;h3 id="why-separation-matters-again"&gt;Why Separation Matters (Again)
&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve lived both sides of this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With virtualized router (2 years):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rebooting compute = entire house offline&lt;/li&gt;
&lt;li&gt;Hardware failure = no internet until fixed&lt;/li&gt;
&lt;li&gt;Testing new containers = risk to network stability&lt;/li&gt;
&lt;li&gt;Update that goes wrong = everything down&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;With separated router (4 years total, 2 with current hardware):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reboot compute whenever needed = nobody notices&lt;/li&gt;
&lt;li&gt;NAS drive failure = internet stays up while I fix it&lt;/li&gt;
&lt;li&gt;Container experiment goes wrong = network unaffected&lt;/li&gt;
&lt;li&gt;Proxmox update goes sideways = restore snapshot, internet never dropped&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The peace of mind is worth every penny.&lt;/p&gt;
&lt;h2 id="when-to-ask-for-help"&gt;When to Ask for Help
&lt;/h2&gt;&lt;p&gt;You will get stuck. Everyone does. Here&amp;rsquo;s where to go.&lt;/p&gt;
&lt;h3 id="official-resources"&gt;Official Resources
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;OPNsense Documentation:&lt;/strong&gt; &lt;a class="link" href="https://docs.opnsense.org/" target="_blank" rel="noopener"
&gt;https://docs.opnsense.org/&lt;/a&gt;
Start here. Covers installation, basic config, features, and troubleshooting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPNsense Forum:&lt;/strong&gt; &lt;a class="link" href="https://forum.opnsense.org/" target="_blank" rel="noopener"
&gt;https://forum.opnsense.org/&lt;/a&gt;
Active community. Search before posting. Someone has probably had your exact problem.&lt;/p&gt;
&lt;h3 id="community-resources"&gt;Community Resources
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Reddit:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;r/OPNsenseFirewall - Dedicated to OPNsense specifically&lt;/li&gt;
&lt;li&gt;r/homelab - Broader homelab topics, lots of OPNsense users&lt;/li&gt;
&lt;li&gt;r/selfhosted - Media server and self-hosting focus&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://corelab.tech/content/images/size/w1140/2025/11/OPNsensept1.png" alt="The OPNsense Foundation: Building a Bulletproof Homelab Firewall" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;The OPNsense Foundation: Building a Bulletproof Homelab Firewall (Part 1)&lt;/strong&gt;
Looking for a comprehensive OPNsense setup? Check out this detailed installation and configuration guide from corelab.tech. This two-part series covers everything from initial setup to advanced configuration, providing a complete walkthrough for getting OPNsense running properly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://corelab.tech/opnsensept1/"
class="backlink-button"
target="_self"
&gt;
View Article
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="how-to-ask-for-help-effectively"&gt;How to Ask for Help Effectively
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;When posting for help, include:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What you&amp;rsquo;re trying to do:&lt;/strong&gt; &amp;ldquo;Setting up VLAN for IoT devices&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What you&amp;rsquo;ve tried:&lt;/strong&gt; &amp;ldquo;Created VLAN 3 on switch and OPNsense, enabled DHCP&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What&amp;rsquo;s happening:&lt;/strong&gt; &amp;ldquo;Devices get IP but can&amp;rsquo;t access internet&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relevant logs:&lt;/strong&gt; Screenshots or text of firewall logs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firewall rules:&lt;/strong&gt; Screenshots of rules for that interface&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Bad post:&lt;/strong&gt; &amp;ldquo;My VLANs don&amp;rsquo;t work, help!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good post:&lt;/strong&gt; &amp;ldquo;IoT VLAN devices get DHCP but can&amp;rsquo;t reach internet. Created VLAN 3, enabled DHCP on 10.0.3.1/24, firewall rules allow ports 80/443 outbound. Logs show blocked packets to 8.8.8.8. Screenshots attached.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The good post gets answered quickly. The bad post gets ignored or gets &amp;ldquo;need more information&amp;rdquo; responses.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom Line
&lt;/h2&gt;&lt;p&gt;After 15 years of consumer routers and 6 years running OPNsense, I&amp;rsquo;ll never go back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What you give up:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One-box convenience&lt;/li&gt;
&lt;li&gt;The &amp;ldquo;plug-and-play&amp;rdquo; feel (initially)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Network segmentation that contains breaches&lt;/li&gt;
&lt;li&gt;Real intrusion detection that shows you threats&lt;/li&gt;
&lt;li&gt;Full traffic visibility, you know what&amp;rsquo;s happening&lt;/li&gt;
&lt;li&gt;Actual control over your network&lt;/li&gt;
&lt;li&gt;Reliability that doesn&amp;rsquo;t degrade over time&lt;/li&gt;
&lt;li&gt;Independence from your compute infrastructure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Is it more work?&lt;/strong&gt; Yes, initially. The first few weekends require learning and configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is it worth it?&lt;/strong&gt; Absolutely.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re building infrastructure worth thousands of dollars. Protecting it with a $120 consumer router that randomly reboots is like putting a $20 padlock on a bank vault.&lt;/p&gt;
&lt;p&gt;The peace of mind alone is worth it. No more &amp;ldquo;why is the internet down?&amp;rdquo; conversations. No more daily reboots. No more wondering if your network is secure.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll know it&amp;rsquo;s secure because you built it that way.&lt;/p&gt;
&lt;h2 id="real-numbers-cost-over-time"&gt;Real Numbers: Cost Over Time
&lt;/h2&gt;&lt;p&gt;Let me show you the actual economics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer router approach (what I did for 15 years):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New router every 2.5 years: $150 average&lt;/li&gt;
&lt;li&gt;15 years = 6 routers = $900&lt;/li&gt;
&lt;li&gt;Plus time troubleshooting, rebooting, replacing&lt;/li&gt;
&lt;li&gt;Security: Hope and prayer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Dedicated router approach (what I&amp;rsquo;m doing now):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Budget build: $200 initial investment&lt;/li&gt;
&lt;li&gt;Expected lifespan: 5+ years (I&amp;rsquo;m at 8+ years across two hardware generations)&lt;/li&gt;
&lt;li&gt;Electricity: ~15W vs 30-50W consumer router (actually saves money)&lt;/li&gt;
&lt;li&gt;Security: Real, verifiable, logged&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you replaced the budget build every 5 years, you&amp;rsquo;d spend $200 every 5 years ($40/year) versus $150 every 2.5 years ($60/year).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You save money while getting massively better security and reliability.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My current Protectli cost $400. Over 5 years, that&amp;rsquo;s $80/year. Still cheaper than consumer routers, and I expect it to last longer than 5 years based on how the previous hardware held up.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="whats-next"&gt;What&amp;rsquo;s Next
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve got your router sorted. Your network is segmented and secure. You can work on your compute infrastructure without taking down the internet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In Part 3, we build the vault:&lt;/strong&gt; A proper NAS that won&amp;rsquo;t lose your data when drives fail. We&amp;rsquo;ll talk about storage options like MergerFS + SnapRaid, ZFS, and off the shelf options, proper backup strategies, and why your data deserves more respect than a single external hard drive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025" &gt;Read Part 3: Your Storage Deserves Its Own Box&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="quick-reference-opnsense-commands"&gt;Quick Reference: OPNsense Commands
&lt;/h2&gt;&lt;p&gt;Keep this handy for common tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reboot:&lt;/strong&gt; System → Power → Reboot&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apply firewall changes:&lt;/strong&gt; Firewall → Rules → Apply Changes (the orange banner)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check blocked traffic:&lt;/strong&gt; Firewall → Log Files → Live View&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;See active connections:&lt;/strong&gt; Firewall → Diagnostics → States Dump&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check DHCP leases:&lt;/strong&gt; Services → DHCPv4 → Leases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDS/IPS alerts:&lt;/strong&gt; Services → Intrusion Detection → Alerts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup config:&lt;/strong&gt; System → Configuration → Backups → Download configuration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test connectivity:&lt;/strong&gt; Diagnostics → Ping (or Diagnostics → Traceroute)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check interface status:&lt;/strong&gt; Interfaces → Overview&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View system logs:&lt;/strong&gt; System → Log Files → General&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="series-navigation"&gt;Series Navigation
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-hardware-guide-2025" &gt;Part 1 - The 3 Boxes That Run Your Media Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-router-2025" &gt;Part 2 - Why Your ISP Router Is Killing Your Stack&lt;/a&gt; ← You are here&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025" &gt;Part 3 - Your Storage Deserves Its Own Box&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-compute-2025" &gt;Part 4 - Proxmox Is Your New Best Friend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-backup-2025" &gt;Part 5 - Backups That Work&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Home Media Server Hardware Guide: The 4-Box Setup</title><link>https://diymediaserver.com/post/media-server-hardware-guide-2025/</link><pubDate>Wed, 29 Oct 2025 07:25:02 -0600</pubDate><guid>https://diymediaserver.com/post/media-server-hardware-guide-2025/</guid><description>&lt;img src="https://diymediaserver.com/post/media-server-hardware-guide-2025/featured_hu_9df989f75f692623.webp" alt="Featured image of post Home Media Server Hardware Guide: The 4-Box Setup" /&gt;&lt;p&gt;Everyone starts with the dream: one box to rule them all. Media apps, storage, downloads, and yes, even routing if you&amp;rsquo;re feeling extra ambitious.&lt;/p&gt;
&lt;p&gt;Then reality hits. Reboot time becomes a nightmare. LXCs start before storage mounts. Apps can&amp;rsquo;t find their configs. Nothing works unless you babysit the boot order like it&amp;rsquo;s a toddler who found the scissors drawer.&lt;/p&gt;
&lt;p&gt;The fix?&lt;br&gt;
Four boxes.&lt;br&gt;
Clear roles.&lt;br&gt;
Real uptime.&lt;/p&gt;
&lt;p&gt;Ask me how I know.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: It involves a VM NFS Server, a Jellyfin LXC, and a very angry Sunday afternoon where I almost threw my server out the window.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="link" href="https://diymediaserver.com/post/why_i_ditched_my_vm_nas_and_went_bare-metal/" target="_blank" rel="noopener"
&gt;Why I Ditched My VM NAS and Went Bare-Metal&lt;/a&gt;&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
A stable home media server needs four dedicated machines: one for routing ($150-$500), one for storage ($400-$1900), one to run your apps ($200-$1400), and one for backups ($150-$400). Total investment: $900-$4,200. Expected build time: 2-4 weekends. The payoff: 99% uptime instead of the 80% you&amp;rsquo;re getting now.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="who-this-guide-is-for"&gt;Who This Guide Is For
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;You&amp;rsquo;re the right person for this if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re running everything on a single machine and it&amp;rsquo;s becoming a headache&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ve hit the &amp;ldquo;nothing works after a reboot&amp;rdquo; wall&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re ready to invest $900-$4,200 and a few weekends for a reliable setup&lt;/li&gt;
&lt;li&gt;You want to learn Proxmox, networking, and proper homelab architecture&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re tired of your family asking why the streaming server is down again&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;You might want something else if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You only want to watch a few movies and don&amp;rsquo;t care about uptime (get a Synology and call it done)&lt;/li&gt;
&lt;li&gt;You have zero interest in learning Linux or networking basics&lt;/li&gt;
&lt;li&gt;Your budget is under $900 total (stick with a single good machine for now)&lt;/li&gt;
&lt;li&gt;You need this working by next weekend (this is a learning journey, so take your time)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Skills you&amp;rsquo;ll need (or learn along the way):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic Linux command line comfort. You can navigate directories and edit files&lt;/li&gt;
&lt;li&gt;Willingness to read documentation when things break. And they will&lt;/li&gt;
&lt;li&gt;Basic networking concepts. What&amp;rsquo;s an IP address, what&amp;rsquo;s a subnet&lt;/li&gt;
&lt;li&gt;Patience for the learning curve. Expect some frustration, especially around Proxmox LXCs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&amp;rsquo;t worry if you&amp;rsquo;re not an expert. I wasn&amp;rsquo;t either. You&amp;rsquo;ll learn by doing, and I&amp;rsquo;ll point you to resources when you need them.&lt;/p&gt;
&lt;h2 id="the-real-roi-what-this-costs-you"&gt;The Real ROI: What This Costs You
&lt;/h2&gt;&lt;p&gt;Yes, you&amp;rsquo;re about to spend $900-$4,200 on hardware. That sounds like a lot until you do the math on what you&amp;rsquo;re already spending.&lt;/p&gt;
&lt;h3 id="the-streaming-service-tax"&gt;The Streaming Service Tax
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s what you&amp;rsquo;re probably paying right now:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Netflix (Standard)&lt;/td&gt;
&lt;td&gt;$17.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disney+ &amp;amp; Hulu&lt;/td&gt;
&lt;td&gt;$19.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max (HBO)&lt;/td&gt;
&lt;td&gt;$18.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prime Video&lt;/td&gt;
&lt;td&gt;$8.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple TV+&lt;/td&gt;
&lt;td&gt;$12.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peacock&lt;/td&gt;
&lt;td&gt;$10.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paramount+&lt;/td&gt;
&lt;td&gt;$13.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starz&lt;/td&gt;
&lt;td&gt;$4.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crunchyroll&lt;/td&gt;
&lt;td&gt;$7.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$116.41/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That&amp;rsquo;s &lt;strong&gt;$1,396.92 per year&lt;/strong&gt;. Every year. &lt;strong&gt;For-ev-er&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s getting worse. Remember when Netflix was $7.99? Yeah, so do I.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t have streaming services? Cable averages about $100/month too.&lt;/p&gt;
&lt;p&gt;Have streaming services and cable? Wow. This guide can help you the most.&lt;/p&gt;
&lt;h3 id="the-break-even-math"&gt;The Break-Even Math
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Budget build ($900):&lt;/strong&gt;&lt;br&gt;
You break even in 8 months. After that, you&amp;rsquo;re saving over $1,400/year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mid-range build ($1,900):&lt;/strong&gt;&lt;br&gt;
Break even in 16 months. Then it&amp;rsquo;s pure savings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dream build ($4,200):&lt;/strong&gt;&lt;br&gt;
Break even in 36 months. But you&amp;rsquo;ve built something that&amp;rsquo;ll last 5-9 years.&lt;/p&gt;
&lt;h3 id="but-wait-theres-more"&gt;But Wait, There&amp;rsquo;s More
&lt;/h3&gt;&lt;p&gt;The math above assumes you cancel &lt;em&gt;everything&lt;/em&gt;. Realistically, you&amp;rsquo;ll probably keep 1-2 services. I keep Prime Video for the rare night my Jellyfin server is down.&lt;/p&gt;
&lt;p&gt;Even if you only cancel 6 out of 9 services, you&amp;rsquo;re saving $80+/month. That&amp;rsquo;s $960/year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plus you get:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content you actually want to watch (not algorithmic slop)&lt;/li&gt;
&lt;li&gt;No more &amp;ldquo;sorry, that&amp;rsquo;s leaving the platform in 3 days&amp;rdquo;&lt;/li&gt;
&lt;li&gt;No more &amp;ldquo;this show is on Peacock now, not Netflix&amp;rdquo;&lt;/li&gt;
&lt;li&gt;No more paying for multiple services to watch one show&lt;/li&gt;
&lt;li&gt;Complete control over quality (no streaming compression)&lt;/li&gt;
&lt;li&gt;Offline access (because internet outages happen)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-hidden-costs-they-dont-tell-you"&gt;The Hidden Costs They Don&amp;rsquo;t Tell You
&lt;/h3&gt;&lt;p&gt;Streaming services love to fragment. That show you&amp;rsquo;re watching? Season 1-3 are on Netflix. Season 4 moved to Paramount+. The spinoff is Max-exclusive. The movie that inspired it all is on Disney+.&lt;/p&gt;
&lt;p&gt;You end up subscribing to four services to watch one franchise. Or you play subscription roulette, canceling and re-subscribing monthly and praying you remember to cancel before they charge you again.&lt;/p&gt;
&lt;p&gt;With your own server? You watch what you want, when you want, without playing platform musical chairs.&lt;/p&gt;
&lt;h3 id="the-power-bill-reality-check"&gt;The Power Bill Reality Check
&lt;/h3&gt;&lt;p&gt;&amp;ldquo;But what about electricity?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Fair point. The math:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average 4-box stack: ~130W idle, ~220W under load&lt;/li&gt;
&lt;li&gt;Running 24/7 at $0.12/kWh: ~$12-16/month&lt;/li&gt;
&lt;li&gt;So your actual monthly cost: $12-16 in power&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;re still saving $80-90/month vs. streaming services. That&amp;rsquo;s $960-$1,080/year in your pocket.&lt;/p&gt;
&lt;h3 id="what-about-content-acquisition"&gt;What About Content Acquisition?
&lt;/h3&gt;&lt;p&gt;Look, I&amp;rsquo;m not going to tell you how to get your &amp;ldquo;Linux ISOs&amp;rdquo;. But if you&amp;rsquo;re already paying for content through other means (totally legal Blu-ray ripping, of course), you&amp;rsquo;re not adding costs. You&amp;rsquo;re organizing what you already own.&lt;/p&gt;
&lt;p&gt;And if you&amp;rsquo;ve got a library card? Many libraries offer free streaming through Hoopla, Kanopy, or similar services. Rip, organize, and keep forever. All legal, all free.&lt;/p&gt;
&lt;p&gt;I have several guides on all of this. Here are a few:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/torrent-vs-usenet/" &gt;Torrent vs Usenet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to_install-sabnzbd-in-docker/" &gt;How to Install SABnzbd in Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/building-your-own-legal-media-collection/" &gt;Building Your Own Legal Media Collection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-intangible-benefits"&gt;The Intangible Benefits
&lt;/h3&gt;&lt;p&gt;This is about more than money. It&amp;rsquo;s about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not having shows you&amp;rsquo;re watching get canceled mid-season&lt;/li&gt;
&lt;li&gt;Not dealing with ads (even on &amp;ldquo;ad-free&amp;rdquo; tiers that still show promos)&lt;/li&gt;
&lt;li&gt;Not having your smart TV spy on your viewing habits&lt;/li&gt;
&lt;li&gt;Not being at the mercy of licensing deals and regional restrictions&lt;/li&gt;
&lt;li&gt;Owning your media instead of renting it monthly&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The bottom line:&lt;/strong&gt; Even the dream build pays for itself in 3 years. After that, you&amp;rsquo;re saving $1,400/year while having a better experience, more control, and media that&amp;rsquo;s yours.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&amp;rsquo;s not a cost. That&amp;rsquo;s an investment that keeps paying dividends.&lt;/p&gt;
&lt;h2 id="section-1-why-one-box-isnt-enough"&gt;SECTION 1: Why One Box Isn&amp;rsquo;t Enough
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the classic mistake. You cram everything (Jellyfin, Sonarr, Radarr, qBittorrent, SABnzbd, storage, Docker, and maybe even OPNsense if you&amp;rsquo;re really ambitious) onto that old desktop collecting dust in your closet.&lt;/p&gt;
&lt;p&gt;It works.&lt;br&gt;
Until it doesn&amp;rsquo;t.&lt;/p&gt;
&lt;h3 id="heres-what-goes-wrong"&gt;Here&amp;rsquo;s what goes wrong:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;One reboot = full outage.&lt;/strong&gt;&lt;br&gt;
Your apps don&amp;rsquo;t only go offline. They could come back in the wrong order. Jellyfin can&amp;rsquo;t see your media because NFS hasn&amp;rsquo;t mounted yet. Sonarr can&amp;rsquo;t write downloads because the share isn&amp;rsquo;t ready. That boot-time race between your LXC containers and your storage mounts? Nobody wins. Especially not you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance becomes a bottleneck.&lt;/strong&gt;&lt;br&gt;
You&amp;rsquo;re trying to transcode a 4K stream while ZFS is scrubbing, torrents and nzbs are verifying, and your CPU is melting down. Everything competes for the same I/O, RAM, and CPU cores. There&amp;rsquo;s no isolation, only a noisy soup of services fighting for resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security is non-existent.&lt;/strong&gt;&lt;br&gt;
Mixing your media stack with your router or storage means one compromised container could break your entire network or corrupt your storage. Ever had Jellyfin accidentally nuke a mounted drive path? I have.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting becomes impossible.&lt;/strong&gt;&lt;br&gt;
Is it Jellyfin&amp;rsquo;s fault? A bad NFS mount? A network issue? You don&amp;rsquo;t know, because everything&amp;rsquo;s tangled together. Diagnosing one problem means taking down everything else to test it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can&amp;rsquo;t upgrade or experiment safely.&lt;/strong&gt;&lt;br&gt;
Want to try that new transcoding setup? Too bad. You&amp;rsquo;re scared to touch anything because a mistake means your entire network goes down. You&amp;rsquo;re paralyzed, stuck with a setup you hate because changing it is too risky.&lt;/p&gt;
&lt;h3 id="my-breaking-point"&gt;My Breaking Point
&lt;/h3&gt;&lt;p&gt;I ran everything on one computer for years. It worked. Mostly. Until the power supply died.&lt;/p&gt;
&lt;p&gt;The server went silent. And because I ran OPNsense, Jellyfin, and my NAS all on the same computer, I had no internet and no access to my media.&lt;/p&gt;
&lt;p&gt;Meanwhile my partner asked increasingly pointed questions about when the internet would be back up.&lt;/p&gt;
&lt;p&gt;Four hours and a trip to BestBuy later, I got it back up and running.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when I split the stack. Router, storage, compute, backup. Four boxes. The next power-supply failure took down exactly one of them, not all four.&lt;/p&gt;
&lt;h2 id="section-2-the-four-box-philosophy"&gt;SECTION 2: The Four-Box Philosophy
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the core idea. Separate traffic, storage, compute, and backup into four dedicated machines.&lt;/p&gt;
&lt;p&gt;Each machine does one job. Does it well. Stays stable. And doesn&amp;rsquo;t bring down the others when it needs maintenance.&lt;/p&gt;
&lt;h3 id="the-stack"&gt;The Stack:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Box 1: Router&lt;/strong&gt; ($150-$500)&lt;br&gt;
Controls all traffic, handles DNS, manages VLANs, runs your firewall. This is the gatekeeper.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Box 2: Storage&lt;/strong&gt; ($400-$1900)&lt;br&gt;
Holds all your media, provides redundancy, serves files to everything else. This is your vault.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Box 3: Compute&lt;/strong&gt; ($200-$1400)&lt;br&gt;
Runs all your apps: Jellyfin, Sonarr, Radarr, and any new software you want to try. This is the chaos layer where VMs and containers change, break, and get rebuilt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Box 4: Backup&lt;/strong&gt; ($150-$400)&lt;br&gt;
Backs up your compute node&amp;rsquo;s LXCs and VMs. Something breaks after a change? Roll back to the last working snapshot. This is your insurance policy.&lt;/p&gt;
&lt;h3 id="why-this-works"&gt;Why This Works:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Modularity = Sanity.&lt;/strong&gt;&lt;br&gt;
Reboot compute without touching storage. Upgrade your NAS without breaking Jellyfin. Swap routers without tearing down your entire stack. When one piece fails, the others keep running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clear Failure Domains.&lt;/strong&gt;&lt;br&gt;
Something breaks? You know exactly where to look. Service down? Check compute. Files missing? Check storage. Network problems? Check the router. No more digging through monolithic logs trying to find which piece is the problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Easy Scaling.&lt;/strong&gt;&lt;br&gt;
Need more storage? Add drives to the NAS. Need more CPU power? Upgrade the compute box. Want faster networking? Upgrade the router. You&amp;rsquo;re not locked into some Frankenstein all-in-one that needs replacing entirely to improve one thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Safe Experimentation.&lt;/strong&gt;&lt;br&gt;
Want to try Tdarr? Spin up an LXC. Want to test a new Jellyfin build? Clone your container. Breaking something on the compute box doesn&amp;rsquo;t take down your network or risk your data.&lt;/p&gt;
&lt;h2 id="section-3-box-1--the-router-your-gatekeeper"&gt;SECTION 3: Box 1 – The Router (Your Gatekeeper)
&lt;/h2&gt;&lt;p&gt;Your router is the traffic cop of your entire media stack. If it sucks, everything downstream suffers.&lt;/p&gt;
&lt;p&gt;And yes, your ISP&amp;rsquo;s all-in-one Wi-Fi &amp;ldquo;router&amp;rdquo; absolutely sucks.&lt;/p&gt;
&lt;h3 id="why-a-dedicated-router-matters"&gt;Why a Dedicated Router Matters:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;You need control, not guesswork.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Static IPs for all your services so they don&amp;rsquo;t randomly change&lt;/li&gt;
&lt;li&gt;VLANs to isolate your download clients from the rest of your network&lt;/li&gt;
&lt;li&gt;Firewall rules to keep risky devices in their own sandbox&lt;/li&gt;
&lt;li&gt;DNS that resolves &lt;code&gt;jellyfin.home&lt;/code&gt; instead of forcing you to remember IPs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might get some of that from a consumer &amp;ldquo;gaming&amp;rdquo; router with racing stripes. You will get all of it from a real box running OPNsense or pfSense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security starts here.&lt;/strong&gt;&lt;br&gt;
A proper firewall lets you isolate containers, restrict traffic by IP, and run reverse proxies safely. You can see what&amp;rsquo;s happening through logs and alerts. You&amp;rsquo;re not flying blind hoping nothing bad gets through.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your media traffic stays fast.&lt;/strong&gt;&lt;br&gt;
4K Jellyfin streams shouldn&amp;rsquo;t fight with torrent downloads and game updates for bandwidth. Smart routing and QoS keep your streams smooth even when everything else is maxed out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Network isolation becomes possible.&lt;/strong&gt;&lt;br&gt;
Keep your kids&amp;rsquo; devices off the same subnet as your torrent client. Put Home Assistant on a separate VLAN with no access to your NAS. Isolate your IoT lightbulbs from anything that matters. This is how you build defense in depth.&lt;/p&gt;
&lt;h3 id="what-youll-do-with-it"&gt;What You&amp;rsquo;ll Do With It:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Set up static DHCP reservations for every device in your stack&lt;/li&gt;
&lt;li&gt;Create VLANs for different trust levels (trusted, media, IoT, guest)&lt;/li&gt;
&lt;li&gt;Configure firewall rules to control what can talk to what&lt;/li&gt;
&lt;li&gt;Set up a reverse proxy (Nginx Proxy Manager or Traefik) for clean URLs&lt;/li&gt;
&lt;li&gt;Monitor traffic to see what&amp;rsquo;s hogging bandwidth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A real router box gives you something an ISP combo unit never will: a place to grow into. The hardware below will run OPNsense comfortably, with dual 2.5G ports and more CPU than routing alone will ever ask for.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0H1Q23GW4"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/Aoostar_hu_58f6c56c74837faf.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/Aoostar_hu_8c766b935d7e97b7.webp" width="600" height="498" alt="Glorlin Ryzen 7 8745HS Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glorlin Ryzen 7 8745HS Mini PC&lt;/strong&gt;&lt;br&gt;
An 8-core/16-thread mini PC with dual 2.5G LAN, so OPNsense installs without an add-in card and an IDS like Suricata has CPU to spare. It&amp;rsquo;s a good deal more machine than routing needs, which is the trade you&amp;rsquo;re making while cheap dual-NIC boxes are thin on the ground.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4fByupD" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Coming in Part 2:&lt;/strong&gt; I&amp;rsquo;ll walk through which routers to buy, from an $80 used SFF box that punches above its weight to the $500+ &amp;ldquo;final boss&amp;rdquo; build that handles 10Gbps like it&amp;rsquo;s nothing.&lt;/p&gt;
&lt;h2 id="section-4-box-2--storage-your-vault"&gt;SECTION 4: Box 2 – Storage (Your Vault)
&lt;/h2&gt;&lt;p&gt;Your media has to live somewhere. Your download folder on a single SSD isn&amp;rsquo;t going to cut it when you&amp;rsquo;re storing terabytes of movies and TV shows.&lt;/p&gt;
&lt;p&gt;This is where the NAS comes in. It holds everything, keeps it safe, and makes sure it&amp;rsquo;s still there tomorrow when you wake up.&lt;/p&gt;
&lt;h3 id="why-storage-deserves-its-own-box"&gt;Why Storage Deserves Its Own Box:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;You need redundancy.&lt;/strong&gt;&lt;br&gt;
Hard drives fail. It&amp;rsquo;s not &lt;em&gt;if&lt;/em&gt;, it&amp;rsquo;s &lt;em&gt;when&lt;/em&gt;. (Ask me about my dead 14TB drive that took 9TB of unwatched shows with it.) A proper NAS setup, whether ZFS, SnapRAID, Unraid, or mirrored drives, gives you fault tolerance. You lose a drive without losing your media. Your compute server shouldn&amp;rsquo;t be the thing keeping data alive. That&amp;rsquo;s not its job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You need speed and consistency.&lt;/strong&gt;&lt;br&gt;
Streaming 4K, transcoding, downloading, and seeding all at once? You need fast sequential reads and smart caching. A NAS optimized for this workload handles it better than random drives thrown into your compute box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything needs to find files reliably.&lt;/strong&gt;&lt;br&gt;
When storage is its own box, it boots first, mounts clean, and stays available. Your LXCs don&amp;rsquo;t wonder if &lt;code&gt;/mnt/media&lt;/code&gt; is ready. They connect via NFS or SMB and work. No more boot-order mysteries or race conditions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You need upgrade flexibility.&lt;/strong&gt;&lt;br&gt;
Rebuilding your Jellyfin container? The NAS doesn&amp;rsquo;t care. Upgrading your compute box? Your media stays untouched. Separating storage from compute means you can break, upgrade, or rebuild services without risking your files.&lt;/p&gt;
&lt;h3 id="what-youll-do-with-it-1"&gt;What You&amp;rsquo;ll Do With It:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Set up RAID or ZFS pools for redundancy&lt;/li&gt;
&lt;li&gt;Configure NFS or SMB shares for your media libraries&lt;/li&gt;
&lt;li&gt;Set up automated scrubs to catch bit rot early (weekly &lt;code&gt;zpool scrub tank&lt;/code&gt; cron is a good baseline)&lt;/li&gt;
&lt;li&gt;Monitor drive health with SMART data (&lt;code&gt;smartctl -a /dev/sdX&lt;/code&gt; and &lt;code&gt;smartd&lt;/code&gt; alerts)&lt;/li&gt;
&lt;li&gt;Plan your backup strategy. Yes, RAID is not a backup&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-boot-order-secret"&gt;The Boot Order Secret:
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s the key: &lt;strong&gt;Router boots first. Storage boots second. Compute boots third.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This order matters. When storage is ready before compute starts, your LXCs never race against mount points. Everything works. This alone eliminates 90% of the &amp;ldquo;why isn&amp;rsquo;t Jellyfin seeing my files&amp;rdquo; problems.&lt;/p&gt;
&lt;p&gt;A dedicated NAS does more than hold extra drives. It keeps your data alive when a power blip kills a disk mid-write.&lt;/p&gt;
&lt;p&gt;For a drive that fits this role, here&amp;rsquo;s what I&amp;rsquo;d stack in a multi-bay enclosure today:&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CSPCFKR9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ironwolf-24tb_hu_fb22fc25aed7cd30.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ironwolf-24tb_hu_1da8908f73983a8e.webp" width="600" height="869" alt="Seagate IronWolf Pro 24TB NAS Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate IronWolf Pro 24 TB&lt;/strong&gt;&lt;br&gt;
Built for NAS enclosures that never sleep. 7200 RPM CMR with a 512 MB cache and a 550 TB/year workload rating, so you can stack three or four of these in a ZFS or SnapRAID pool without cooking them. 5-year warranty and 3 years of Seagate Rescue included, because when (not if) one dies, you want a parachute.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4cSZxv5" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Coming in Part 3:&lt;/strong&gt; The actual NAS builds, from budget SnapRAID setups using old hardware to dream ZFS rigs that&amp;rsquo;ll make you feel like you work at a data center.&lt;/p&gt;
&lt;h2 id="section-5-box-3--compute-your-workhorse"&gt;SECTION 5: Box 3 – Compute (Your Workhorse)
&lt;/h2&gt;&lt;p&gt;This is the fun box. The one that runs everything: Jellyfin, Sonarr, Radarr, Tdarr, Bazarr, Lidarr, and whatever new &lt;code&gt;-arr&lt;/code&gt; appears next week.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s where things happen. But it only works &lt;em&gt;because&lt;/em&gt; the other boxes are doing their jobs.&lt;/p&gt;
&lt;h3 id="why-compute-needs-its-own-home"&gt;Why Compute Needs Its Own Home:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s where the mess lives.&lt;/strong&gt;&lt;br&gt;
App updates. LXC templates. Docker networks. Transcoding jobs. This is the chaotic, ever-changing layer of your stack. You want it isolated so you can blow things up, reboot, or rebuild without touching storage or breaking your network.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You get proper service isolation.&lt;/strong&gt;&lt;br&gt;
Run Jellyfin in one LXC and qBittorrent in another. Experiment with Bazarr or Prowlarr without wrecking your stable setup. Proxmox makes this easy whether you&amp;rsquo;re using full VMs or lean LXCs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You have actual resource control.&lt;/strong&gt;&lt;br&gt;
Assign specific RAM, CPU cores, and disk I/O to each container. Want Tdarr to use every core you&amp;rsquo;ve got overnight but stay quiet during the day? Done. Want to make sure qBittorrent can&amp;rsquo;t steal resources from Jellyfin during movie night? Easy. Good luck getting that kind of control on a janky all-in-one setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It&amp;rsquo;s built to change.&lt;/strong&gt;&lt;br&gt;
Want GPU passthrough for transcoding? Add it. Need more RAM? Upgrade this one box. This is the only part of your stack that &lt;em&gt;should&lt;/em&gt; change regularly, so build it to take hits and keep going.&lt;/p&gt;
&lt;h3 id="pets-vs-cattle-why-this-changes-everything"&gt;Pets vs Cattle: Why This Changes Everything
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s a concept from DevOps that&amp;rsquo;ll change how you think about your services:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pets&lt;/strong&gt; are servers you name, nurture, and baby. When they get sick, you stay up all night nursing them back to health. You&amp;rsquo;re terrified to reboot them because something might not come back right. You&amp;rsquo;ve spent hours configuring them and the thought of rebuilding makes you break out in a cold sweat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cattle&lt;/strong&gt; are servers you number, not name. If one gets sick, you cull it and spin up a replacement. They&amp;rsquo;re built from templates, configured automatically, and completely disposable. Lose one? Who cares. You&amp;rsquo;ve got backups and can rebuild in minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your all-in-one box? That&amp;rsquo;s a pet.&lt;/strong&gt; You&amp;rsquo;re scared to touch it. Every update is a gamble. You&amp;rsquo;ve probably got configs and tweaks you can&amp;rsquo;t even remember making. If it dies, you&amp;rsquo;re spending days rebuilding from memory and hoping you didn&amp;rsquo;t forget anything critical.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This four-box setup? It lets you treat services as cattle.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage is separate = data is safe.&lt;/strong&gt;&lt;br&gt;
Your media, configs, and databases live on the NAS. When you rebuild a container, all your important data is still there. You&amp;rsquo;re not risking your files every time you experiment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proxmox makes containers disposable.&lt;/strong&gt;&lt;br&gt;
Spin up an LXC in 2 minutes. Test something. Hate it? Delete it. No drama. Want to try a different Jellyfin version? Clone your container, test it, keep what works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backups make everything replaceable.&lt;/strong&gt;&lt;br&gt;
With Proxmox Backup Server (coming in Part 5), you can restore any container to any point in time with one click. Accidentally nuke your Sonarr config? Restore yesterday&amp;rsquo;s backup. Takes 3 minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-world example from my stack:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I wanted to test Jellyfin 10.11.0 (major changes in this update), but I was running 10.10.7 in production. Old me (one-box setup) would&amp;rsquo;ve been too scared to upgrade. What if it breaks? What if I can&amp;rsquo;t roll back?&lt;/p&gt;
&lt;p&gt;New me (four-box setup):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Snapshot my Jellyfin LXC (30 seconds)&lt;/li&gt;
&lt;li&gt;Clone it to a test container (2 minutes)&lt;/li&gt;
&lt;li&gt;Upgrade the test version, break things, learn stuff&lt;/li&gt;
&lt;li&gt;Decide the new version wasn&amp;rsquo;t ready for Ubuntu yet&lt;/li&gt;
&lt;li&gt;Delete the test container, keep running production&lt;/li&gt;
&lt;li&gt;Total risk to my family&amp;rsquo;s movie night: zero&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;This is the mentality shift that makes homelabbing fun instead of terrifying.&lt;/strong&gt; You stop being afraid to experiment because nothing you do can truly break things. Your data is safe on the NAS. Your configs are backed up. Your containers are cattle.&lt;/p&gt;
&lt;p&gt;When your Nginx Proxy Manager container gets weird? Nuke it. Restore from last night&amp;rsquo;s backup. Six minutes later you&amp;rsquo;re back online and you didn&amp;rsquo;t break a sweat.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the power of separation. Your compute box becomes a playground instead of a house of cards.&lt;/p&gt;
&lt;h3 id="what-youll-do-with-it-2"&gt;What You&amp;rsquo;ll Do With It:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Install Proxmox VE as your hypervisor&lt;/li&gt;
&lt;li&gt;Create LXC containers for each service (Jellyfin, AudioBookshelf, Pi-hole, etc.)&lt;/li&gt;
&lt;li&gt;Create VMs for specific Docker stacks (Arr Suite, Immich, etc.)&lt;/li&gt;
&lt;li&gt;Mount your NFS shares from the storage box (&lt;code&gt;/etc/fstab&lt;/code&gt; entries or Proxmox storage configs)&lt;/li&gt;
&lt;li&gt;Set up GPU passthrough for hardware transcoding&lt;/li&gt;
&lt;li&gt;Configure automated container backups&lt;/li&gt;
&lt;li&gt;Learn to love (and occasionally hate) Proxmox&amp;rsquo;s web interface&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-learning-curve"&gt;The Learning Curve:
&lt;/h3&gt;&lt;p&gt;Proxmox has a reputation. It&amp;rsquo;s powerful but not exactly hand-holdy. You will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Screw up your first few LXCs (everyone does)&lt;/li&gt;
&lt;li&gt;Accidentally delete something important (keep backups)&lt;/li&gt;
&lt;li&gt;Spend an hour Googling why NFS mounts aren&amp;rsquo;t working (permissions, always permissions)&lt;/li&gt;
&lt;li&gt;Eventually figure it out and feel like a genius&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is normal. This is the path. Embrace it.&lt;/p&gt;
&lt;p&gt;The CPU you pick here matters more than you&amp;rsquo;d think. Hardware transcoding, container density, and snappy web UIs all live or die by single-thread speed and integrated graphics.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel Core i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel Core i5-14500 14th Generation Desktop Processor&lt;/strong&gt;
This CPU is ideal for Proxmox homelabs. Strong single-thread performance, 14 cores for efficient virtualization, and UHD 770 graphics good enough to handle Jellyfin hardware transcoding with QSV.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Coming in Part 4:&lt;/strong&gt; The actual Proxmox builds, from repurposed office PCs to custom rigs, plus the LXC templates and configurations that work.&lt;/p&gt;
&lt;h2 id="section-6-how-they-all-work-together"&gt;SECTION 6: How They All Work Together
&lt;/h2&gt;&lt;p&gt;Four random boxes on a shelf is a pile of e-waste. Four boxes with clear jobs and a defined boot order is a system. Here&amp;rsquo;s how mine fit together.&lt;/p&gt;
&lt;h3 id="the-flow"&gt;The Flow:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Router boots first.&lt;/strong&gt;&lt;br&gt;
It brings up the network, handles DHCP, starts DNS. Everything else depends on this foundation. It&amp;rsquo;s the traffic cop and bouncer rolled into one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage boots second.&lt;/strong&gt;&lt;br&gt;
With the network ready, your NAS comes online, mounts its drives, starts serving files via NFS/SMB. Now the vault is open and ready.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compute boots third.&lt;/strong&gt;&lt;br&gt;
With networking and storage available, Proxmox fires up, LXCs start in order, apps connect to their storage mounts, and everything works. No race conditions. No mysteries. Reliable boots.&lt;/p&gt;
&lt;h3 id="when-something-breaks"&gt;When Something Breaks:
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Router problem?&lt;/strong&gt; Network is down, but storage and compute are fine. Swap in a backup router (or fix the issue), restore config, back online.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage problem?&lt;/strong&gt; Media streaming stops, but your network still works. You can troubleshoot, check drives, restore from backup. Apps keep running even if they can&amp;rsquo;t access files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compute problem?&lt;/strong&gt; Services go down, but your network and data are safe. Reboot it. Restore a container from backup. Rebuild from scratch if needed. Your files and network never blinked.&lt;/p&gt;
&lt;p&gt;This is the magic of separation. Problems stay contained. Fixes don&amp;rsquo;t cascade. You&amp;rsquo;re not gambling with your entire setup every time something hiccups.&lt;/p&gt;
&lt;h2 id="section-7-the-fourth-box---backups"&gt;SECTION 7: The Fourth Box - Backups
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve got your router. Your storage. Your compute. Feels solid, right?&lt;/p&gt;
&lt;p&gt;Until something breaks. Then it&amp;rsquo;s panic mode. Unless you have Box 4: Backup Server.&lt;/p&gt;
&lt;h3 id="proxmox-backup-server--the-box-that-saves-you"&gt;Proxmox Backup Server – The Box That Saves You
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;re running VMs or LXCs, you need backups that actually work. Not rsync scripts you never test. Not drive clones you made once in 2023. Real, restorable, tested backups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What PBS does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automated daily backups of all your containers and VMs&lt;/li&gt;
&lt;li&gt;Deduplication so you can keep weeks of snapshots without filling drives&lt;/li&gt;
&lt;li&gt;Versioned backups you can browse by date&lt;/li&gt;
&lt;li&gt;One-click restores directly from Proxmox&amp;rsquo;s interface&lt;/li&gt;
&lt;li&gt;Verification jobs to make sure backups actually work (run &lt;code&gt;proxmox-backup-manager verify-job run&lt;/code&gt; on a schedule)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Real-world save:&lt;/strong&gt;&lt;br&gt;
I once destroyed the wrong LXC in Proxmox (RIP Nginx Proxy Manager). PBS had me back online in 7 minutes. Without it, I&amp;rsquo;d have spent hours rebuilding configs from memory and old screenshots.&lt;/p&gt;
&lt;h3 id="is-this-required"&gt;Is This Required?
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Technically?&lt;/strong&gt; No. You can run without it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practically?&lt;/strong&gt; If your Jellyfin container died tomorrow, could you restore it in under 10 minutes? If not, you need this.&lt;/p&gt;
&lt;h3 id="the-budget-reality"&gt;The Budget Reality:
&lt;/h3&gt;&lt;p&gt;You can run PBS on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A $150 used mini PC with a 2TB drive&lt;/li&gt;
&lt;li&gt;Your NAS if it has extra resources (though I prefer separation)&lt;/li&gt;
&lt;li&gt;Even a Raspberry Pi for smaller setups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is cheap insurance. Way cheaper than rebuilding your entire stack from scratch on a work night.&lt;/p&gt;
&lt;p&gt;A fast SSD as the PBS datastore makes a huge difference for restore time. This is the drive I&amp;rsquo;m running mine on:&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BYSKXGJV"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/teamgroup-2tb_hu_3526b05897fc2870.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/teamgroup-2tb_hu_a4ed59a9a0b2a4f7.webp" width="600" height="420" alt="TEAMGROUP T-Force Vulcan Z 2TB SATA SSD" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TEAMGROUP T-Force Vulcan Z 2TB&lt;/strong&gt;
This is the SSD I use for my Proxmox Backup Server datastore. I back up 6 VMs and 12 LXCs and I&amp;rsquo;m only at 11% capacity. At this rate I&amp;rsquo;ll fill it in 3 years. PBS deduplication is amazing.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B01KUBazn" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Coming in Part 5:&lt;/strong&gt; Setting up Proxmox Backup Server, automating backups, and actually testing your restores. So you&amp;rsquo;re not learning how backups work during a crisis.&lt;/p&gt;
&lt;h2 id="common-questions"&gt;Common Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I start with only 2 boxes?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Start with compute + storage, use your existing router. Or start with router + compute, use attached storage temporarily. The point is separation, not perfection. You can grow into the full stack.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What if I already have a Synology?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Perfect. That&amp;rsquo;s your storage box. You&amp;rsquo;re already halfway there. Focus on getting a dedicated router and compute box, connect them to your Synology, and you&amp;rsquo;ve got the stack.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I really need to learn Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not technically. You could run Docker on bare metal. But Proxmox gives you isolation, snapshots, easy backups, and the ability to run multiple services without them stepping on each other. The learning curve pays off.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What about power consumption?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Fair question. Four boxes use more power than one. My stack pulls about 130W idle. That&amp;rsquo;s $11-13/month at $0.12/kWh. The uptime and sanity are worth it. If you&amp;rsquo;re power-sensitive, choose efficient hardware (covered in the build guides).&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use old hardware for this?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Absolutely. Some of the best home servers are built from old office PCs. The next posts will cover budget builds using used hardware alongside new builds for those who want to buy fresh.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="what-about-the-easy-button"&gt;What About the Easy Button?
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;Can&amp;rsquo;t I buy a Synology and run everything there?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can. And for some people, that&amp;rsquo;s the right answer. If you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don&amp;rsquo;t want to learn Linux or networking&lt;/li&gt;
&lt;li&gt;Only want it to work out of the box&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t mind being locked into Synology&amp;rsquo;s ecosystem&lt;/li&gt;
&lt;li&gt;Are okay with limited customization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then yes, get a Synology DS925+ or similar, install Jellyfin/Plex, and call it done.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C8S7SF4B"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/synology-ds925_hu_eda617831103a258.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/synology-ds925_hu_7d3ee7d460962e5f.webp" width="600" height="427" alt="Synology 4-Bay DiskStation DS925&amp;#43; (Diskless)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Synology 4-Bay DiskStation DS925+ (Diskless)&lt;/strong&gt;&lt;br&gt;
Want an easy button? Here you go. The Synology DS925+ is an OK all-in-one box. Enough storage and processing power for your media services, but it&amp;rsquo;ll run out of steam the moment you go beyond basic needs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4805Mdd" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You give up flexibility for that convenience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can&amp;rsquo;t easily run multiple isolated services.&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t experiment without risk.&lt;/li&gt;
&lt;li&gt;You can&amp;rsquo;t scale components independently.&lt;/li&gt;
&lt;li&gt;And when it breaks, you&amp;rsquo;re at Synology&amp;rsquo;s mercy for support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guide is for people who want more control, are willing to learn, and understand that power comes with responsibility (and occasional Googling).&lt;/p&gt;
&lt;h2 id="your-growth-path"&gt;Your Growth Path
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Phase 1: Get it working&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build all four boxes&lt;/li&gt;
&lt;li&gt;Get basic services running (Jellyfin, Sonarr, Radarr)&lt;/li&gt;
&lt;li&gt;Verify everything boots reliably&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Phase 2: Stabilize and monitor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add Proxmox Backup Server&lt;/li&gt;
&lt;li&gt;Set up proper monitoring (Uptime Kuma, Grafana)&lt;/li&gt;
&lt;li&gt;Document your setup (seriously, do this)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Phase 3: Optimize and expand&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add a GPU for hardware transcoding&lt;/li&gt;
&lt;li&gt;Upgrade to 10GbE networking between boxes&lt;/li&gt;
&lt;li&gt;Add more storage as your library grows&lt;/li&gt;
&lt;li&gt;Experiment with new services in isolated LXCs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Phase 4: Advanced features&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPN for remote access&lt;/li&gt;
&lt;li&gt;Automated media management (Recyclarr, Tdarr)&lt;/li&gt;
&lt;li&gt;Advanced monitoring and alerting&lt;/li&gt;
&lt;li&gt;Offsite backup replication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don&amp;rsquo;t need to do all of this at once. Build the foundation, then grow into it.&lt;/p&gt;
&lt;p&gt;The next four posts break down each box in detail: what to buy, what to avoid, and budget vs. dream builds.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Budget Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Router&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Traffic control, DNS, VLANs, firewall&lt;/td&gt;
&lt;td&gt;$150-$500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliable media storage with redundancy&lt;/td&gt;
&lt;td&gt;$400-$1900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs all your apps and services&lt;/td&gt;
&lt;td&gt;$200-$1400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saves you from disaster&lt;/td&gt;
&lt;td&gt;$150-$400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each box has different needs, different trade-offs, and yes, different price tags. Here&amp;rsquo;s the thing though: you don&amp;rsquo;t need the dream build to get started. A $900 budget stack works. A $4,200 dream stack works better. Both work way better than cramming everything onto one box and hoping for the best.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Part 2 drops next week: The Router.&lt;/strong&gt; I&amp;rsquo;ll walk through which hardware to buy, from an $80 used SFF box that punches above its weight to the $500+ &amp;ldquo;final boss&amp;rdquo; build that handles 10Gbps like it&amp;rsquo;s nothing. Plus OPNsense installation, VLAN setup, and the firewall rules that matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to be ready?&lt;/strong&gt; Start thinking about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your budget for each component&lt;/li&gt;
&lt;li&gt;What old hardware you might already have&lt;/li&gt;
&lt;li&gt;Whether you want to buy used or new&lt;/li&gt;
&lt;li&gt;How much redundancy you need&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And most importantly: what you want to stop babysitting, so you can finally watch your media in peace.&lt;/p&gt;
&lt;h2 id="series-navigation"&gt;Series Navigation
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-hardware-guide-2025/" &gt;Part 1 - The 4 Boxes That Run Your Media Server (2025 Edition)&lt;/a&gt; ← You are here&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-router-2025/" &gt;Part 2 - Why Your ISP Router Is Killing Your Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-storage-2025/" &gt;Part 3 - Your Storage Deserves Its Own Box&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-compute-2025/" &gt;Part 4 - Proxmox Is Your New Best Friend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/media-server-backup-2025/" &gt;Part 5 - Backups That Work (With PBS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Proxmox LXC UID/GID Mapping and lxc.idmap Guide</title><link>https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/</link><pubDate>Mon, 20 Oct 2025 07:03:02 -0600</pubDate><guid>https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/</guid><description>&lt;img src="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_ecbe1e25324d49f.webp" alt="Featured image of post Proxmox LXC UID/GID Mapping and lxc.idmap Guide" /&gt;&lt;p&gt;If you&amp;rsquo;ve ever shared files between your Proxmox host and an LXC container and wondered why permissions look &amp;ldquo;off&amp;rdquo; or why bind mounts throw permission errors, you&amp;rsquo;ve already bumped into UID/GID mapping. It&amp;rsquo;s the behind-the-scenes mechanism that keeps your containers safe. It&amp;rsquo;s also where confusing file ownership problems come from.&lt;/p&gt;
&lt;p&gt;UID (User ID) and GID (Group ID) mapping is what stops a container&amp;rsquo;s root from being &lt;em&gt;actual&lt;/em&gt; root on the host. It&amp;rsquo;s also what lets you share directories sanely without handing over the keys to the kingdom.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the thing: UID/GID mapping sounds intimidating. It isn&amp;rsquo;t. Once you get the concept, it stops being a mystery and starts being a tool. We&amp;rsquo;ll walk through how it works in Proxmox LXCs, how to customize it, and how to fix the headaches it causes. Especially for media server setups like Jellyfin.&lt;/p&gt;
&lt;p&gt;And don&amp;rsquo;t fall for the easy out of switching to a privileged container. You&amp;rsquo;ll trade real security for a few minutes of convenience.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Proxmox isolates unprivileged LXC containers by mapping their user and group IDs to high-numbered counterparts on the host, starting at 100000. This security feature prevents container root users from escalating to host root. You can customize these mappings by editing &lt;code&gt;/etc/pve/lxc/&amp;lt;ID&amp;gt;.conf&lt;/code&gt; and adjusting &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt;. Doing so allows seamless file sharing and predictable permissions when bind mounting directories inside containers.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel® Core™ i5-14500 14th Generation Desktop Processor&lt;/strong&gt;
This CPU is ideal for Proxmox homelabs, providing strong single-thread performance and efficient virtualization across 14 cores, perfect for experimenting with LXC containers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-uidgid-mapping-matters"&gt;Why UID/GID Mapping Matters
&lt;/h2&gt;&lt;p&gt;User and group IDs in Linux define ownership and permissions. UID 0 is root. UID 1000 might be your first host user. Normally these IDs are global across a single OS instance. Containers complicate that picture. Without remapping, &amp;ldquo;root&amp;rdquo; inside a container would also be UID 0 on the host. If someone broke out of that container, they&amp;rsquo;d own your box.&lt;/p&gt;
&lt;p&gt;Proxmox solves this by defaulting to unprivileged containers. The container&amp;rsquo;s UID/GID space gets &lt;em&gt;remapped&lt;/em&gt; to a non-privileged range on the host. Think of it as giving each container its own private numbering system that translates to harmless IDs on the host side.&lt;/p&gt;
&lt;h3 id="visualizing-the-mapping"&gt;Visualizing the Mapping
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s what happens with default mapping:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Container Namespace Host System
┌────────────────────┐ ┌──────────────────┐
│ UID 0 (root) │ ───&amp;gt; │ UID 100000 │
│ UID 1 (daemon) │ ───&amp;gt; │ UID 100001 │
│ UID 1000 (user) │ ───&amp;gt; │ UID 101000 │
│ UID 65535 (nobody) │ ───&amp;gt; │ UID 165535 │
└────────────────────┘ └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With custom mapping for media server access:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Container Namespace Host System
┌────────────────────┐ ┌──────────────────┐
│ UID 0 (root) │ ───&amp;gt; │ UID 100000 │
│ UID 999 │ ───&amp;gt; │ UID 100999 │
│ UID 1000 (user) │ ───&amp;gt; │ UID 1000 │ ← Direct map!
│ UID 1001 │ ───&amp;gt; │ UID 101001 │
└────────────────────┘ └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="why-100000-as-the-starting-point"&gt;Why 100000 as the Starting Point?
&lt;/h3&gt;&lt;p&gt;Proxmox chose 100000 as the default offset for good reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;System UIDs (0-999):&lt;/strong&gt; Reserved for system services and the root user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User UIDs (1000-60000):&lt;/strong&gt; Normal user accounts on most Linux systems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container offset (100000+):&lt;/strong&gt; Far enough from everything else to avoid conflicts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So even if you have 99 regular users on your host (UID 1000-1099), their IDs will never collide with container-mapped IDs.&lt;/p&gt;
&lt;h3 id="privileged-vs-unprivileged-what-you-need-to-know"&gt;Privileged vs Unprivileged: What You Need to Know
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Unprivileged Containers&lt;/strong&gt; (&lt;code&gt;unprivileged: 1&lt;/code&gt; - Default):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UID/GID mapping is enforced by the kernel&lt;/li&gt;
&lt;li&gt;Container root = UID 100000 on host (harmless)&lt;/li&gt;
&lt;li&gt;Recommended for 99% of use cases including media servers&lt;/li&gt;
&lt;li&gt;Security isolation is maintained even if container is compromised&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Privileged Containers&lt;/strong&gt; (&lt;code&gt;unprivileged: 0&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NO UID/GID mapping&lt;/li&gt;
&lt;li&gt;Container root = actual root on host&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Major security risk:&lt;/strong&gt; Container escape = full host compromise&lt;/li&gt;
&lt;li&gt;Rarely needed for media servers, avoid unless absolutely necessary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Jellyfin, Plex, and similar media servers, always use unprivileged containers with proper UID mapping. Performance is identical. Security is night and day.&lt;/p&gt;
&lt;h2 id="understanding-the-default-mapping"&gt;Understanding the Default Mapping
&lt;/h2&gt;&lt;p&gt;When you create a new unprivileged LXC container in Proxmox, it sets up UID/GID mappings automatically to keep the container isolated from the host.&lt;/p&gt;
&lt;p&gt;Check your container&amp;rsquo;s configuration file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/pve/lxc/101.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;arch: amd64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cores: &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hostname: jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;memory: &lt;span class="m"&gt;2048&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rootfs: local-lvm:vm-101-disk-0,size&lt;span class="o"&gt;=&lt;/span&gt;8G
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unprivileged: &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;unprivileged: 1&lt;/code&gt; line triggers automatic UID/GID offset mapping defined in &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; on the host.&lt;/p&gt;
&lt;h3 id="the-default-mapping-files"&gt;The Default Mapping Files
&lt;/h3&gt;&lt;p&gt;Check your host&amp;rsquo;s subordinate ID files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/subuid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:100000:65536
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container UID 0 → Host UID 100000&lt;/li&gt;
&lt;li&gt;Container UID 1 → Host UID 100001&lt;/li&gt;
&lt;li&gt;Container UID 1000 → Host UID 101000&lt;/li&gt;
&lt;li&gt;Container UID 65535 → Host UID 165535&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that&amp;rsquo;s why your media files show weird ownership. When your Jellyfin container creates or accesses files, they appear on the host owned by UIDs in the 100000+ range instead of your normal user account. Not what you want.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
With serious I/O and flexible storage, this mini-workstation is an excellent homelab node for advanced LXC container setups, allowing you to experiment with lxc.idmap and /etc/subgid in a high-performance, space-saving package.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites
&lt;/h2&gt;&lt;p&gt;Before configuring UID/GID mapping, ensure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You&amp;rsquo;re running Proxmox VE 7.0 or later&lt;/li&gt;
&lt;li&gt;Container is stopped (or not yet created)&lt;/li&gt;
&lt;li&gt;You know your host user&amp;rsquo;s UID (run &lt;code&gt;id&lt;/code&gt; on host)&lt;/li&gt;
&lt;li&gt;You have root access to Proxmox host&lt;/li&gt;
&lt;li&gt;Media files exist on host with known ownership&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-by-step-setting-up-uidgid-mapping-for-media-servers"&gt;Step-by-Step: Setting Up UID/GID Mapping for Media Servers
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the practical walk-through for getting Jellyfin (or Plex, Emby, etc.) talking to your media library.&lt;/p&gt;
&lt;h3 id="step-1-identify-your-host-media-directory-ownership"&gt;Step 1: Identify Your Host Media Directory Ownership
&lt;/h3&gt;&lt;p&gt;First, check who owns your media files on the host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -ln /media/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -ln /media/tv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see output like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt; Oct &lt;span class="m"&gt;20&lt;/span&gt; 10:30 /media/movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;1000 1000&lt;/code&gt; shows UID and GID. That&amp;rsquo;s typically your primary user account. Write this number down. You&amp;rsquo;ll need it later.&lt;/p&gt;
&lt;h3 id="step-2-stop-your-container"&gt;Step 2: Stop Your Container
&lt;/h3&gt;&lt;p&gt;If the container is already running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct stop &lt;span class="m"&gt;101&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Skip this step if you&amp;rsquo;re configuring a new container that hasn&amp;rsquo;t started yet.&lt;/p&gt;
&lt;h3 id="step-3-edit-the-container-configuration"&gt;Step 3: Edit the Container Configuration
&lt;/h3&gt;&lt;p&gt;Open the container&amp;rsquo;s config file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/pve/lxc/101.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add these lines at the end:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Our mapping covers exactly 65,536 IDs (0-65535):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Line 1: 1000 IDs (container 0-999)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Line 2: 1 ID (container 1000)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Line 3: 64535 IDs (container 1001-65535)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Total: 1000 + 1 + 64535 = 65,536 ✓&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maps container UIDs 0-999 to host UIDs 100000-100999 (keeps container root isolated)&lt;/li&gt;
&lt;li&gt;Maps container UID 1000 directly to host UID 1000 (your user)&lt;/li&gt;
&lt;li&gt;Maps container GID 1000 directly to host GID 1000 (your user&amp;rsquo;s group)&lt;/li&gt;
&lt;li&gt;Resumes offset mapping at UID 1001 → host UID 101001 (100000 base + 1001 offset), maintaining security isolation for all remaining users&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save and exit (Ctrl+X, Y, Enter).&lt;/p&gt;
&lt;h3 id="step-4-add-bind-mounts-for-your-media"&gt;Step 4: Add Bind Mounts for Your Media
&lt;/h3&gt;&lt;p&gt;Add your media directories to the config file (if not already open from Step 3, use &lt;code&gt;nano /etc/pve/lxc/101.conf&lt;/code&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /media/movies,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp1: /media/tv,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/tv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp2: /media/music,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/music
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or use the command line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -mp0 /media/movies,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -mp1 /media/tv,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/tv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-5-verify-subordinate-uid-files"&gt;Step 5: Verify Subordinate UID Files
&lt;/h3&gt;&lt;p&gt;Check that your host has the necessary UID range:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/subuid
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see at least:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:100000:65536
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now edit both files and add this line at the top:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# In /etc/subuid:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:1000:1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# In /etc/subgid:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:1000:1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;What this means:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allows the root user (who runs LXC) to map a single ID (1000) directly through to the container&lt;/li&gt;
&lt;li&gt;Change 1000 to match your host user&amp;rsquo;s UID/GID (find it with the &lt;code&gt;id&lt;/code&gt; command)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-6-start-and-configure-container"&gt;Step 6: Start and Configure Container
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct start &lt;span class="m"&gt;101&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enter the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct enter &lt;span class="m"&gt;101&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create or modify the user to match your host UID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check if jellyfin user already exists&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id jellyfin 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If user doesn&amp;#39;t exist yet, create it:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;useradd -u &lt;span class="m"&gt;1000&lt;/span&gt; -m jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If user already exists with wrong UID, modify it:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;usermod -u &lt;span class="m"&gt;1000&lt;/span&gt; jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;usermod -g &lt;span class="m"&gt;1000&lt;/span&gt; jellyfin &lt;span class="c1"&gt;# Also update primary group&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-7-verify-permissions"&gt;Step 7: Verify Permissions
&lt;/h3&gt;&lt;p&gt;Test that Jellyfin can access your media:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;su - jellyfin -s /bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /media/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;touch /media/movies/test.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If these commands work without &amp;ldquo;Permission denied&amp;rdquo; errors, you&amp;rsquo;re good!&lt;/p&gt;
&lt;p&gt;On the host, verify:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -ln /media/movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Remove the test file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm /media/movies/test.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Files created by Jellyfin should show UID 1000, matching your host user.&lt;/p&gt;
&lt;h3 id="step-8-configure-jellyfin-libraries"&gt;Step 8: Configure Jellyfin Libraries
&lt;/h3&gt;&lt;p&gt;Hit Jellyfin&amp;rsquo;s web interface (usually &lt;code&gt;http://container-ip:8096&lt;/code&gt;) and add your media libraries pointing to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/media/movies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media/tv&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media/music&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Jellyfin scans and accesses everything without permission errors.&lt;/p&gt;
&lt;h2 id="common-media-server-patterns"&gt;Common Media Server Patterns
&lt;/h2&gt;&lt;h3 id="pattern-1-single-user-media-library-most-common"&gt;Pattern 1: Single User Media Library (Most Common)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; All media owned by one user (UID 1000)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /media,mp&lt;span class="o"&gt;=&lt;/span&gt;/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="pattern-2-multiple-media-users"&gt;Pattern 2: Multiple Media Users
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; Media owned by different users (UID 1000, 1001, 1002)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1003&lt;/span&gt; &lt;span class="m"&gt;101003&lt;/span&gt; &lt;span class="m"&gt;64533&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1003&lt;/span&gt; &lt;span class="m"&gt;101003&lt;/span&gt; &lt;span class="m"&gt;64533&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /media,mp&lt;span class="o"&gt;=&lt;/span&gt;/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="troubleshooting-media-server-permission-issues"&gt;Troubleshooting Media Server Permission Issues
&lt;/h2&gt;&lt;h3 id="-problem-jellyfin-cant-see-media-files"&gt;➤ Problem: Jellyfin Can&amp;rsquo;t See Media Files
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Library scan shows no files or &amp;ldquo;Permission denied&amp;rdquo; in logs&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check mapping is active&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- cat /proc/self/uid_map
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Verify file ownership&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- ls -ln /media/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check Jellyfin user UID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- id jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ensure they match your mapping&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="-problem-cant-write-subtitles-or-metadata"&gt;➤ Problem: Can&amp;rsquo;t Write Subtitles or Metadata
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Jellyfin complains about read-only filesystem&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Verify mount isn&amp;#39;t read-only&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/pve/lxc/101.conf &lt;span class="p"&gt;|&lt;/span&gt; grep mp0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Should NOT have &amp;#39;ro&amp;#39; option&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Correct: mp0: /media,mp=/media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Wrong: mp0: /media,mp=/media,ro&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Verify write permissions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- su - jellyfin -s /bin/bash -c &lt;span class="s2"&gt;&amp;#34;touch /media/test.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="-problem-transcoding-fails-with-permission-errors"&gt;➤ Problem: Transcoding Fails with Permission Errors
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Playback works but transcoding fails&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check transcode directory permissions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- ls -ln /var/lib/jellyfin/transcodes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Should be owned by jellyfin user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- chown -R jellyfin:jellyfin /var/lib/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If using host directory for transcoding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -mp1 /fast-storage/transcodes,mp&lt;span class="o"&gt;=&lt;/span&gt;/transcodes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="-problem-files-show-as-uid-100000-on-host"&gt;➤ Problem: Files Show as UID 100000 on Host
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt; Host can&amp;rsquo;t manage files created by Jellyfin&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Your mapping isn&amp;rsquo;t working. Go back to Step 3 and verify your &lt;code&gt;lxc.idmap&lt;/code&gt; configuration, then restart the container.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0C96FR8YC"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="GMKtec Mini PC i7-1185G7" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;GMKtec Mini PC i7-1185G7&lt;/strong&gt;
A compact quad-core system that ships with 16 GB of RAM and a 1 TB SSD, which is enough to run Proxmox with several LXC containers and practice UID/GID mapping on real hardware instead of a throwaway VM.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/454pyTM" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="advanced-running-multiple-media-servers"&gt;Advanced: Running Multiple Media Servers
&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;re running both Jellyfin and Plex, or want separate containers for different media types:&lt;/p&gt;
&lt;h3 id="strategy-shared-mapping-separate-containers"&gt;Strategy: Shared Mapping, Separate Containers
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Container 101 (Jellyfin):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /media,mp&lt;span class="o"&gt;=&lt;/span&gt;/media,ro
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Container 102 (Plex):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64535&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /media,mp&lt;span class="o"&gt;=&lt;/span&gt;/media,ro
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both containers can read the same media library. Use the &lt;code&gt;ro&lt;/code&gt; (read-only) flag to prevent accidental modifications.&lt;/p&gt;
&lt;h2 id="understanding-lxcidmap-syntax"&gt;Understanding lxc.idmap Syntax
&lt;/h2&gt;&lt;p&gt;For those who want to customize beyond these common patterns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: &lt;span class="o"&gt;[&lt;/span&gt;u&lt;span class="p"&gt;|&lt;/span&gt;g&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;first_id_in_container&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;first_id_on_host&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;number_of_ids&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Example breakdown:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt; = UID mapping (use &lt;code&gt;g&lt;/code&gt; for GID)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; = Start at container UID 0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;100000&lt;/code&gt; = Map to host UID 100000&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1000&lt;/code&gt; = Map 1000 consecutive IDs (0-999)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Critical rules:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You must map all 65536 IDs (0-65535)&lt;/li&gt;
&lt;li&gt;No gaps allowed&lt;/li&gt;
&lt;li&gt;No overlaps within same container&lt;/li&gt;
&lt;li&gt;Order matters. Later lines override earlier ones&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Will UID mapping slow down my media server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. UID mapping happens in the kernel with negligible overhead. Your Jellyfin performance will be identical to a privileged container, but much more secure.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use the same mapping for multiple containers?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. All your media-related containers (Jellyfin, Sonarr, Radarr, etc.) can use the same UID mapping pattern. This makes file sharing between them seamless.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What if my media is owned by a different UID?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;Adjust the mapping. If your media is owned by UID 1500 instead of 1000, change:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: u 1500 1500 1&lt;/code&gt;&lt;br&gt;
&lt;code&gt;lxc.idmap: g 1500 1500 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;And adjust the other ranges accordingly to ensure you still map all 65536 IDs.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need to map GID separately from UID?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, if your files rely on group permissions. Most media setups work fine mapping both UID and GID the same way (as shown in the examples).&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I change mapping on an existing Jellyfin container?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;p&gt;Yes, but be prepared to fix file ownership:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Stop container&lt;/li&gt;
&lt;li&gt;Modify mapping in config&lt;/li&gt;
&lt;li&gt;Start container&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;chown -R jellyfin:jellyfin /var/lib/jellyfin&lt;/code&gt; inside container&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your media files on the host should maintain correct ownership since they&amp;rsquo;re already owned by your user.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I ever use a privileged container for Jellyfin?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. There&amp;rsquo;s no legitimate performance or functionality reason to run Jellyfin in a privileged container. Proper UID mapping gives you the same capabilities with vastly better security.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-media-servers-and-uid-mapping"&gt;Conclusion: Media Servers and UID Mapping
&lt;/h2&gt;&lt;p&gt;UID/GID mapping is what makes secure media servers in Proxmox LXC containers practical. The default isolation protects your host. Custom mapping gives you direct access to media libraries without breaking that isolation.&lt;/p&gt;
&lt;h3 id="your-media-server-checklist"&gt;Your Media Server Checklist
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Identify media directory ownership on host (usually UID 1000)&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;lxc.idmap&lt;/code&gt; to map that UID directly&lt;/li&gt;
&lt;li&gt;Add bind mounts for media directories&lt;/li&gt;
&lt;li&gt;Create or modify media server user to match mapped UID&lt;/li&gt;
&lt;li&gt;Test file access from inside container&lt;/li&gt;
&lt;li&gt;Verify host sees correct ownership&lt;/li&gt;
&lt;li&gt;Configure media server libraries&lt;/li&gt;
&lt;li&gt;Run a scan and test playback&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="key-takeaways-for-media-servers"&gt;Key Takeaways for Media Servers
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Use unprivileged containers always.&lt;/strong&gt; The security benefit is enormous, and the setup is straightforward once you understand UID mapping.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Map your user UID directly.&lt;/strong&gt; The single-user passthrough pattern (mapping UID 1000 to 1000) handles 90% of media server scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document your setup.&lt;/strong&gt; Write down which UIDs you mapped and why. You&amp;rsquo;ll thank yourself during troubleshooting or migration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test before going live.&lt;/strong&gt; Verify file access, transcoding, and metadata writing all work before importing your entire library.&lt;/p&gt;
&lt;p&gt;Spend an hour on UID/GID mapping now. Save yourself days of permission-denied logs later.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
This affordable, energy-efficient single-board computer is perfect for beginners wanting to learn about Proxmox LXC containers and UID/GID mappings in a low-risk, hands-on way.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="sources"&gt;Sources
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/wiki/Unprivileged_LXC_containers" target="_blank" rel="noopener"
&gt;Proxmox LXC and Unprivileged Containers Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://man7.org/linux/man-pages/man7/namespaces.7.html" target="_blank" rel="noopener"
&gt;Linux Kernel User Namespace Implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/jellyfin-archive/jellyfin-docs/blob/master/general/administration/hardware-acceleration.md#va-api-and-qsv-hardware-acceleration-on-lxc-or-lxd-container" target="_blank" rel="noopener"
&gt;Jellyfin Documentation: Hardware Acceleration and Permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Tdarr Setup Guide: Install, Configure &amp; Transcode (2026)</title><link>https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/</link><pubDate>Sun, 12 Oct 2025 05:44:47 -0600</pubDate><guid>https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/</guid><description>&lt;img src="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/featured_hu_3eb38f3a6a7f67c7.webp" alt="Featured image of post Tdarr Setup Guide: Install, Configure &amp; Transcode (2026)" /&gt;&lt;p&gt;If you&amp;rsquo;ve ever looked at your media collection and wondered why a 4K movie is eating up 50GB of storage while looking identical to a 10GB version, you&amp;rsquo;re ready for Tdarr. Tdarr handles the tedious work of transcoding your video files, converting them to more efficient formats without sacrificing quality you&amp;rsquo;ll actually notice.&lt;/p&gt;
&lt;p&gt;Tdarr is a complete workflow system. It watches your library, analyzes your files, and optimizes them automatically based on rules you set. Set it up once and it keeps your collection smaller, more compatible, and easier to stream without you touching it again.&lt;/p&gt;
&lt;p&gt;In this guide, I&amp;rsquo;ll walk through setting up Tdarr from scratch. You&amp;rsquo;ll learn how to install it and configure your first Intel QuickSync transcoding workflow. By the end, you&amp;rsquo;ll know how to keep your media library optimized without any ongoing effort from you.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel® Core™ i5-14500 14th Generation Desktop Processor&lt;/strong&gt;
The Intel® Core™ i5-14500 offers strong single-thread performance and built-in Quick Sync hardware transcoding, making it an excellent CPU for running Tdarr and handling multiple concurrent transcodes. It&amp;rsquo;s ideal for DIY homelab builders who want efficient, reliable media processing without needing a discrete GPU. This processor is a great foundation for a Tdarr server.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you manage a large media library, you know the drill: storage fills up fast, and manually transcoding files is about as fun as watching paint dry. This is where Tdarr comes in.&lt;/p&gt;
&lt;p&gt;Tdarr is a transcoding and media management tool that handles the grunt work for you. Instead of babysitting HandBrake for hours, you set up rules once and let Tdarr automatically process your entire video collection in the background. It can remove unwanted audio tracks, compress, re-encode, and standardize files based on whatever criteria you define. The point is reclaiming space. Hard drives keep getting more expensive, so it&amp;rsquo;s worth squeezing what you already have before buying another one.&lt;/p&gt;
&lt;p&gt;The standout feature is &lt;strong&gt;distributed transcoding&lt;/strong&gt;. You can spread the workload across multiple machines, turning that old laptop gathering dust into a transcoding workhorse. Set it up once, let it run, and reclaim terabytes of space without sacrificing playable quality. Plex, Jellyfin, and Emby users get the most out of this.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
To run multiple nodes you&amp;rsquo;ll either need a shared cache and media folder that the server and ALL nodes can access, or a Tdarr Pro subscription for unmapped nodes. I cover both in &lt;a class="link" href="#scaling-out-running-multiple-tdarr-nodes" &gt;Scaling Out: Running Multiple Tdarr Nodes&lt;/a&gt; below.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;This guide only covers installing, configuring, and troubleshooting Tdarr in an unprivileged LXC container. Whether you&amp;rsquo;re new to transcoding or haven&amp;rsquo;t touched it since the early HandBrake days, you&amp;rsquo;ll walk away with a practical setup that saves both time and storage without the usual headaches.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Tdarr automates the tedious job of transcoding and optimizing your media library using flexible, customizable rules. Install it, set up your transcoding preferences through a clean web interface, and optionally scale it across multiple machines for distributed processing. It&amp;rsquo;s particularly valuable for Jellyfin or Plex server owners and anyone looking to reclaim storage space by converting files (like moving from H.264 to H.265) while maintaining quality.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-lxc-instead-of-docker"&gt;Why LXC Instead of Docker?
&lt;/h2&gt;&lt;p&gt;You might be wondering why this guide uses LXC containers instead of Docker, especially since most Tdarr tutorials out there are Docker-based. Here&amp;rsquo;s the reasoning behind this approach:&lt;/p&gt;
&lt;h3 id="direct-hardware-access"&gt;Direct Hardware Access
&lt;/h3&gt;&lt;p&gt;LXC containers run closer to the host system than Docker containers. When you&amp;rsquo;re sharing an iGPU between multiple services (like Jellyfin and Tdarr), LXC makes hardware passthrough significantly more straightforward. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simpler GPU sharing:&lt;/strong&gt; The iGPU can be accessed by multiple LXC containers simultaneously without complex device mapping or privileged modes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native performance:&lt;/strong&gt; LXC containers have almost zero overhead compared to bare metal, making them ideal for transcoding workloads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistent hardware access:&lt;/strong&gt; The GPU stays available even after container restarts without additional Docker flags or volume mounts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="resource-isolation-without-the-overhead"&gt;Resource Isolation Without the Overhead
&lt;/h3&gt;&lt;p&gt;Docker adds multiple layers of abstraction that aren&amp;rsquo;t necessary for long-running services like Tdarr. LXC gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lower memory footprint:&lt;/strong&gt; No Docker daemon consuming resources in the background&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct systemd integration:&lt;/strong&gt; Services start and stop like native system services&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filesystem efficiency:&lt;/strong&gt; No overlay filesystems slowing down video file I/O operations&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="better-integration-with-proxmox"&gt;Better Integration with Proxmox
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;re running Proxmox (which most homelabbers are), LXC is first-class:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native Proxmox management:&lt;/strong&gt; Create, clone, and backup containers through the web UI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot support:&lt;/strong&gt; Take instant snapshots before major configuration changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in monitoring:&lt;/strong&gt; Resource usage graphs and stats without installing additional tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No nested virtualization issues:&lt;/strong&gt; Docker-in-VM can be problematic; LXC runs natively on the host kernel&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="when-docker-makes-more-sense"&gt;When Docker Makes More Sense
&lt;/h3&gt;&lt;p&gt;Docker is optimized for different use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Portability:&lt;/strong&gt; If you need to move Tdarr between different hosts frequently, Docker&amp;rsquo;s image format makes this trivial&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quick testing:&lt;/strong&gt; Spinning up and destroying Docker containers is faster for experimentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Proxmox:&lt;/strong&gt; If you&amp;rsquo;re running on bare Ubuntu/Debian without Proxmox, Docker is simpler to set up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a dedicated media server running on Proxmox where you want maximum transcoding performance and are already sharing GPU resources with other services, LXC is the better architectural choice. You get native performance, simpler hardware access, and better integration with your virtualization platform, all while using fewer system resources.&lt;/p&gt;
&lt;h2 id="understanding-tdarr-and-why-it-matters"&gt;Understanding Tdarr and Why It Matters
&lt;/h2&gt;&lt;p&gt;Before we start spinning up containers and tweaking codec settings, let&amp;rsquo;s get clear on what Tdarr does. And more importantly, what it doesn&amp;rsquo;t do.&lt;/p&gt;
&lt;h3 id="what-is-tdarr"&gt;What Is Tdarr?
&lt;/h3&gt;&lt;p&gt;Tdarr is a conditional transcoding application. It looks at each video file in your library, runs it through a set of rules you define (like &amp;ldquo;Is this still using H.264?&amp;rdquo; or &amp;ldquo;Is the bitrate ridiculously high?&amp;rdquo;), and then decides whether to transcode it, leave it alone, or skip it entirely.&lt;/p&gt;
&lt;p&gt;Picture Tdarr as a quality control inspector on an assembly line. Each file gets examined, processed if needed, and passed along based on the standards you&amp;rsquo;ve set. Behind the scenes, it uses &lt;strong&gt;FFmpeg&lt;/strong&gt; and &lt;strong&gt;HandBrake&lt;/strong&gt; to do the heavy lifting. You get reliable performance and industry-standard compression without becoming a command-line wizard.&lt;/p&gt;
&lt;h3 id="why-use-tdarr"&gt;Why Use Tdarr?
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s why I swear by Tdarr:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storage Efficiency:&lt;/strong&gt; Convert those space-hogging H.264 or MPEG-2 files to modern H.265 (HEVC) and watch your storage usage drop by up to 50% without sacrificing watchable quality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uniform Playability:&lt;/strong&gt; Standardize your codecs and container formats so everything plays smoothly across Plex, Jellyfin, smart TVs, and mobile apps. No more &amp;ldquo;this format is not supported&amp;rdquo; messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set-and-Forget Automation:&lt;/strong&gt; Configure your preferences once, and Tdarr handles the monitoring, processing, and updating automatically. Your media library maintains itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability:&lt;/strong&gt; Add multiple worker nodes for distributed transcoding when you need to process hundreds or thousands of files without waiting weeks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;ve ever watched Jellyfin refuse to play an unsupported codec, or wondered why your server storage is maxed out despite regular cleanup, Tdarr solves both problems for the long haul.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;
A large-capacity hard drive like the Seagate Barracuda 24TB is crucial for storing your growing media library and transcoded files managed by Tdarr. Its reliability and size make it perfect for bulk storage in a homelab or media server environment. This drive ensures you have plenty of space for all your media needs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-1-installing-tdarr-via-unprivileged-lxc"&gt;Step 1: Installing Tdarr via Unprivileged LXC
&lt;/h2&gt;&lt;p&gt;Why an LXC? Because I&amp;rsquo;m sharing my iGPU with Jellyfin who is also installed in an LXC.&lt;/p&gt;
&lt;p&gt;My Jellyfin guide:&lt;br&gt;
&lt;a class="link" href="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/" target="_blank" rel="noopener"
&gt;Jellyfin + Intel QuickSync in Unprivileged LXC - The Complete Guide&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="prerequisites-checklist"&gt;Prerequisites Checklist
&lt;/h3&gt;&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox VE with an unprivileged LXC running Debian 13&lt;/li&gt;
&lt;li&gt;Intel CPU with QuickSync (12th gen or newer recommended)&lt;/li&gt;
&lt;li&gt;Basic command-line familiarity&lt;/li&gt;
&lt;li&gt;Network access to your LXC container&lt;/li&gt;
&lt;li&gt;Media files stored on your Proxmox host with known paths&lt;/li&gt;
&lt;li&gt;At least 8GB RAM allocated to the LXC&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="add-the-debian-non-free-component"&gt;Add the Debian Non-Free Component
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/apt/sources.list.d/debian.sources
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the components line add &lt;code&gt;non-free&lt;/code&gt; to the end.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Types: deb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;URIs: http://deb.debian.org/debian
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Suites: trixie trixie-updates
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Components: contrib main non-free
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Signed-By: /usr/share/keyrings/debian-archive-keyring.gp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="add-a-new-source-for-mkvtoolnix"&gt;Add a new source for MKVToolNix
&lt;/h3&gt;&lt;p&gt;Pull the gpg key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget -O /etc/apt/keyrings/gpg-pub-moritzbunkus.gpg https://mkvtoolnix.download/gpg-pub-moritzbunkus.gpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create a new source File:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/apt/sources.list.d/mkvtoolnix.sources
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Types: deb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;URIs: https://mkvtoolnix.download/debian/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Suites: trixie
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Components: main
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Signed-By: /etc/apt/keyrings/gpg-pub-moritzbunkus.gpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="install-handbrake-ffmpeg-mkvtoolnix-and-other-required-tools"&gt;Install HandBrake, ffmpeg, MKVtoolNix and other required tools
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install -y sudo curl unzip ca-certificates gnupg wget vainfo libva2 intel-media-va-driver-non-free libva-drm2 libva2 pciutils handbrake-cli ffmpeg mkvtoolnix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
I install ffmpeg and handbrake outside of Tdarr because I find it is easier to maintain.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="add-users-and-groups"&gt;Add Users and Groups
&lt;/h3&gt;&lt;p&gt;Create your &lt;code&gt;media&lt;/code&gt; group that has read and write access to your media files. I use &lt;code&gt;media&lt;/code&gt; with a gid of &lt;code&gt;1001&lt;/code&gt; for mine make sure to use yours if it is different.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo addgroup --gid &lt;span class="m"&gt;1001&lt;/span&gt; media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the Tdarr User:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo useradd -r -m -d /opt/tdarr -s /usr/sbin/nologin tdarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the video, render and media groups to the Tdarr user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -aG render,video,media tdarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Reset the group membership without logging out:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo loginctl enable-linger tdarr 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-2-lxc-config-changes"&gt;Step 2: LXC Config Changes
&lt;/h2&gt;&lt;p&gt;Stop the container.
Add these lines to your &lt;code&gt;/etc/pve/lxc/CT#.conf&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Bind mounts your media to the LXC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mp0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/media/Storage/Movies,mp=/media/Movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mp1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/media/Storage/Shows,mp=/media/Shows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Maps permissions to the host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;u 0 100000 65536&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 0 100000 992&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 992 104 1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 992 is the LXC&amp;#39;s Render GID and 104 is the Host&amp;#39;s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 993 100994 7&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1001 1001 1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 1001 is both the LXC&amp;#39;s and Host&amp;#39;s GID for my media group&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.idmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;g 1002 101002 64534&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Passthrough the iGPU&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.mount.entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/dev/dri/ dev/dri/ none bind,optional,create=dir&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;lxc.cgroup2.devices.allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;c 226:* rwm&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here is my Host&amp;rsquo;s &lt;code&gt;/etc/subgid&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:1000:1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:1001:1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:100000:65536
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Render Group&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:104:1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Media Group&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root:1001:1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These lines need to be in here to properly map the GIDs for permissions to work properly.
Restart the container.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Understanding ID Mapping - These &lt;code&gt;lxc.idmap&lt;/code&gt; lines map user and group IDs between your LXC container and the Proxmox host. This allows the &lt;code&gt;tdarr&lt;/code&gt; user inside the container to access your media files and the GPU on the host. The numbers (992, 1001, etc.) must match your specific system. If you&amp;rsquo;re unfamiliar with LXC ID mapping, check out &lt;a class="link" href="https://pve.proxmox.com/wiki/Unprivileged_LXC_containers" target="_blank" rel="noopener"
&gt;Unprivileged LXC containers&lt;/a&gt; first.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-3-install-tdarr"&gt;Step 3: Install Tdarr
&lt;/h2&gt;&lt;p&gt;Now we are ready to install Tdarr&lt;/p&gt;
&lt;p&gt;Navigate to &lt;code&gt;/opt/tdarr&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/tdarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pull the Tdarr install script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wget https://storage.tdarr.io/versions/2.17.01/linux_x64/Tdarr_Updater.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;code&gt;Tdarr_Updater&lt;/code&gt; is a bootstrapper, not a pinned release. Even though the URL says &lt;code&gt;2.17.01&lt;/code&gt;, running it installs the latest stable Tdarr, so you&amp;rsquo;ll land on a current build. That matters if you plan to use newer features like unmapped nodes, which need Tdarr 2.27.01 or later.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Unzip the install package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;unzip Tdarr_Updater.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create two additional directories for Tdarr&amp;rsquo;s temporary files and cache:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir temp cache
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make the script executable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod +x Tdarr_Updater
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run the installer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./Tdarr_Updater
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Run &lt;code&gt;groups tdarr&lt;/code&gt; and verify you see: &lt;code&gt;tdarr video render media&lt;/code&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="initial-tdarr-run"&gt;Initial Tdarr Run
&lt;/h3&gt;&lt;p&gt;Initial Run of Tdarr_Server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/opt/tdarr/Tdarr_Server/Tdarr_Server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let this run for a minute or so. Errors are fine for now. We are creating the JSON config file.&lt;/p&gt;
&lt;p&gt;Initial run of Tdarr_Node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/opt/tdarr/Tdarr_Node/Tdarr_Node
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Again errors are fine.&lt;/p&gt;
&lt;p&gt;Now set the owner for the Tdarr folders:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R tdarr:tdarr /opt/tdarr/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tdarr-server-json-config"&gt;Tdarr Server JSON Config
&lt;/h3&gt;&lt;p&gt;You now have a basic config file for the server. We need to set the &lt;code&gt;serverIP&lt;/code&gt;, &lt;code&gt;handbrakePath&lt;/code&gt;, and &lt;code&gt;ffmpegPath&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /opt/tdarr/configs/Tdarr_Server_Config.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After modifying, it should look something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-JSON" data-lang="JSON"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverPort&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;8266&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;webUIPort&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;8265&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverIP&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;192.168.1.14&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverBindIP&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverDualStack&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;handbrakePath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/bin/HandBrakeCLI&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ffmpegPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/bin/ffmpeg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;logLevel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;INFO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mkvpropeditPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ccextractorPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;openBrowser&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cronPluginUpdate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;auth&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;authSecretKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tsec_NotARealKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;maxLogSizeMB&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;seededApiKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tdarr-node-json-config"&gt;Tdarr Node JSON Config
&lt;/h3&gt;&lt;p&gt;open the basic config file. We need to set &lt;code&gt;serverURL&lt;/code&gt;, &lt;code&gt;ServerIP&lt;/code&gt;, &lt;code&gt;handbrakePath&lt;/code&gt;, &lt;code&gt;ffmpegPath&lt;/code&gt;, and &lt;code&gt;mkvpropeditPath&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /opt/tdarr/configs/Tdarr_Node_Config.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After modifying, it should look something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-JSON" data-lang="JSON"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;nodeName&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;jaded-joey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverURL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://192.168.1.14:8266&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverIP&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;192.168.1.14&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;serverPort&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;8266&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;handbrakePath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/bin/HandBrakeCLI&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;ffmpegPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/bin/ffmpeg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mkvpropeditPath&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/bin/mkvpropedit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;pathTranslators&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;node&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;nodeType&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mapped&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;unmappedNodeCache&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/opt/tdarr/unmappedNodeCache&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;logLevel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;INFO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;priority&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;cronPluginUpdate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;apiKey&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;maxLogSizeMB&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;pollInterval&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;startPaused&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="create-tdarr-services"&gt;Create Tdarr Services
&lt;/h3&gt;&lt;p&gt;To make sure it starts automatically we need to create two services. One for the server and one for the node.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/systemd/system/tdarr-server.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Tdarr Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Service&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/Tdarr_Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_DATA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/configs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_LOGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/logs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/Tdarr_Server/Tdarr_Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-failure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Resource limits (tune as desired)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CPUAccounting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MemoryAccounting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CPUQuota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2G
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Install&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Node&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/systemd/system/tdarr-node.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this (Make sure to change the &lt;code&gt;TDARR_SERVER_HOST&lt;/code&gt; to your IP):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Tdarr Node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr-server.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr-server.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Service&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/Tdarr_Node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_NODE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tdarr-node-01
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_SERVER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.1.14
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_FFMPEG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/ffmpeg
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_LOGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/logs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_TEMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/temp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;TDARR_CACHE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/tdarr/Tdarr_Node/Tdarr_Node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-failure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Resource limits (tune as desired)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CPUAccounting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MemoryAccounting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CPUQuota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;150%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6G
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;IOSchedulingClass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;best-effort
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;IOSchedulingPriority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Install&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="enable-and-start-the-new-services"&gt;Enable and Start the new Services
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Bash" data-lang="Bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; tdarr-server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start tdarr-server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; tdarr-node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start tdarr-node
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check the status of the services to make sure they are running:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status tdarr-server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* tdarr-server.service - Tdarr Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/etc/systemd/system/tdarr-server.service&lt;span class="p"&gt;;&lt;/span&gt; enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active: active &lt;span class="o"&gt;(&lt;/span&gt;running&lt;span class="o"&gt;)&lt;/span&gt; since Wed 2025-10-15 11:15:48 MDT&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; day 20h ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Invocation: 5adc45d529564e898b9e51e29917de75
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Main PID: &lt;span class="m"&gt;7535&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Tdarr_Server&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Tasks: &lt;span class="m"&gt;21&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;limit: 76615&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Memory: 281.5M &lt;span class="o"&gt;(&lt;/span&gt;max: 2G, available: 1.7G, peak: 2G, swap: 31.6M, swap peak: 38.3M&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CPU: 42min 13.105s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CGroup: /system.slice/tdarr-server.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt;-7535 /opt/tdarr/Tdarr_Server/Tdarr_Server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sb"&gt;`&lt;/span&gt;-7541 Tdarr_Server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Node&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status tdarr-node
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;* tdarr-node.service - Tdarr Node
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/etc/systemd/system/tdarr-node.service&lt;span class="p"&gt;;&lt;/span&gt; enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active: active &lt;span class="o"&gt;(&lt;/span&gt;running&lt;span class="o"&gt;)&lt;/span&gt; since Wed 2025-10-15 11:44:19 MDT&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; day 20h ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Invocation: 40a1033a1c984181a6f4c4c399398d4b
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Main PID: &lt;span class="m"&gt;8673&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Tdarr_Node&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Tasks: &lt;span class="m"&gt;187&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;limit: 76615&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Memory: 2.7G &lt;span class="o"&gt;(&lt;/span&gt;max: 6G, available: 3.2G, peak: 6G, swap: 11.3M, swap peak: 184.3M&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CPU: 5h 37min 41.929s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CGroup: /system.slice/tdarr-node.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-4-testing-quicksync"&gt;Step 4: Testing QuickSync
&lt;/h2&gt;&lt;p&gt;Before we go any further we need to make sure QuickSync is working.&lt;/p&gt;
&lt;h3 id="run-this-quick-test"&gt;Run this quick test
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u tdarr env &lt;span class="nv"&gt;LIBVA_DRIVER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;iHD &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; vainfo --display drm --device /dev/dri/renderD128 &lt;span class="p"&gt;|&lt;/span&gt; head -n &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;details class="collapse code" &gt;
&lt;summary&gt;➤ Expected Results&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
libva info: VA-API version 1.22.0
libva info: User environment variable requested driver &amp;#39;iHD&amp;#39;
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_22
libva info: va_openDriver() returns 0
Trying display: drm
vainfo: VA-API version: 1.22 (libva 2.22.0)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 25.2.3 ()
vainfo: Supported profile and entrypoints
VAProfileNone : VAEntrypointVideoProc
VAProfileNone : VAEntrypointStats
VAProfileMPEG2Simple : VAEntrypointVLD
VAProfileMPEG2Simple : VAEntrypointEncSlice
VAProfileMPEG2Main : VAEntrypointVLD
VAProfileMPEG2Main : VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointVLD
VAProfileH264Main : VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointFEI
VAProfileH264Main : VAEntrypointEncSliceLP
VAProfileH264High : VAEntrypointVLD
VAProfileH264High : VAEntrypointEncSlice
VAProfileH264High : VAEntrypointFEI
VAProfileH264High : VAEntrypointEncSliceLP
VAProfileVC1Simple : VAEntrypointVLD
VAProfileVC1Main : VAEntrypointVLD
VAProfileVC1Advanced : VAEntrypointVLD
VAProfileJPEGBaseline : VAEntrypointVLD
VAProfileJPEGBaseline : VAEntrypointEncPicture
VAProfileH264ConstrainedBaseline: VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice
VAProfileH264ConstrainedBaseline: VAEntrypointFEI
VAProfileH264ConstrainedBaseline: VAEntrypointEncSliceLP
VAProfileHEVCMain : VAEntrypointVLD
VAProfileHEVCMain : VAEntrypointEncSlice
VAProfileHEVCMain : VAEntrypointFEI
VAProfileHEVCMain : VAEntrypointEncSliceLP
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileHEVCMain10 : VAEntrypointEncSlice
VAProfileHEVCMain10 : VAEntrypointEncSliceLP
VAProfileVP9Profile0 : VAEntrypointVLD
VAProfileVP9Profile0 : VAEntrypointEncSliceLP
VAProfileVP9Profile1 : VAEntrypointVLD
VAProfileVP9Profile1 : VAEntrypointEncSliceLP
VAProfileVP9Profile2 : VAEntrypointVLD
VAProfileVP9Profile2 : VAEntrypointEncSliceLP
VAProfileVP9Profile3 : VAEntrypointVLD
VAProfileVP9Profile3 : VAEntrypointEncSliceLP
VAProfileHEVCMain12 : VAEntrypointVLD
VAProfileHEVCMain12 : VAEntrypointEncSlice
VAProfileHEVCMain422_10 : VAEntrypointVLD
VAProfileHEVCMain422_10 : VAEntrypointEncSlice
VAProfileHEVCMain422_12 : VAEntrypointVLD
VAProfileHEVCMain422_12 : VAEntrypointEncSlice
VAProfileHEVCMain444 : VAEntrypointVLD
VAProfileHEVCMain444 : VAEntrypointEncSliceLP
VAProfileHEVCMain444_10 : VAEntrypointVLD
VAProfileHEVCMain444_10 : VAEntrypointEncSliceLP
VAProfileHEVCMain444_12 : VAEntrypointVLD
VAProfileHEVCSccMain : VAEntrypointVLD
VAProfileHEVCSccMain : VAEntrypointEncSliceLP
VAProfileHEVCSccMain10 : VAEntrypointVLD
VAProfileHEVCSccMain10 : VAEntrypointEncSliceLP
VAProfileHEVCSccMain444 : VAEntrypointVLD
VAProfileHEVCSccMain444 : VAEntrypointEncSliceLP
VAProfileAV1Profile0 : VAEntrypointVLD&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;If you see &lt;code&gt;Driver version: Intel iHD driver for Intel(R) Gen Graphics - 25.2.3&lt;/code&gt; it is working and we can move on to installing Tdarr.&lt;/p&gt;
&lt;div class="alert alert-default"&gt;
&lt;span class="alert-icon"&gt;ℹ️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Info:&lt;/strong&gt;
Performance Expectations - With Intel QuickSync enabled, a typical 2-hour 1080p movie (H.264 to H.265) transcodes in 15-30 minutes depending on your CPU generation. Without hardware acceleration, the same file could take 2-4 hours. If transcodes are taking longer than expected, verify QuickSync is actually being used by checking GPU utilization: &lt;code&gt;intel_gpu_top&lt;/code&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-5-access-the-web-interface--initial-configuration-walkthrough"&gt;Step 5: Access the web interface &amp;amp; Initial Configuration Walkthrough
&lt;/h2&gt;&lt;p&gt;Open your browser and navigate to: &lt;a class="link" href="http://YourServerIP:8265" target="_blank" rel="noopener"
&gt;http://YourServerIP:8265&lt;/a&gt;.&lt;br&gt;
The Tdarr dashboard should greet you, ready for configuration.&lt;/p&gt;
&lt;p&gt;If it opens CONGRATULATIONS! you have Tdarr up and running.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Security Consideration - Tdarr&amp;rsquo;s web interface (port 8265) has no authentication enabled by default in this configuration. If your server is exposed to the internet, either enable authentication in &lt;code&gt;/opt/tdarr/configs/Tdarr_Server_Config.json&lt;/code&gt; by setting &lt;code&gt;&amp;quot;auth&amp;quot;: true&lt;/code&gt; and configuring credentials, or restrict access using your firewall. For local network-only use, this isn&amp;rsquo;t a concern.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When you first access Tdarr, you&amp;rsquo;ll see the main dashboard with several tabs across the top. Here&amp;rsquo;s what to do first:&lt;/p&gt;
&lt;h3 id="verify-your-node-is-connected"&gt;Verify Your Node is Connected
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Look for the &lt;strong&gt;Nodes&lt;/strong&gt; section on the main page&lt;/li&gt;
&lt;li&gt;You should see your node listed (named something like &amp;ldquo;jaded-joey&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;Click on the node name to see detailed stats&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your node isn&amp;rsquo;t showing up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Double-check the node service is running: &lt;code&gt;sudo systemctl status tdarr-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify the &lt;code&gt;serverIP&lt;/code&gt; matches in both JSON configs&lt;/li&gt;
&lt;li&gt;Check the logs: &lt;code&gt;sudo journalctl -u tdarr-node -f&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what it should look like:
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-nodes_hu_da260dec086cad91.avif 800w, https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-nodes_hu_ca7b40921134c156.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-nodes_hu_1ee3fcb4894b9a4.webp" srcset="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-nodes_hu_1ee3fcb4894b9a4.webp 800w, https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-nodes_hu_53af635c351fd18d.webp 1600w" width="800" height="251" alt="Tdarr Node Example" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="configure-hardware-acceleration"&gt;Configure Hardware Acceleration
&lt;/h3&gt;&lt;p&gt;Before adding libraries, make sure Tdarr knows to use QuickSync:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click on your node in the &lt;strong&gt;Nodes&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Scroll down to &lt;strong&gt;Transcode Options&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Hardware Encoding&lt;/strong&gt;, select &amp;ldquo;Any (nvenc,qsv,vaapi)&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Save the changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This ensures Tdarr uses your iGPU instead of hammering your CPU.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;
The ASRock Intel Arc A580 Challenger provides modern hardware transcoding support (AV1/HEVC/H.264), which is essential for efficient and fast media processing in Tdarr. Two media engines mean it keeps absorbing work as the queue grows, and offloading those tasks from your CPU keeps the rest of the server responsive. A solid way to boost your Tdarr setup for self-hosted media servers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-6-getting-comfortable-with-tdarr"&gt;Step 6: Getting Comfortable With Tdarr
&lt;/h2&gt;&lt;p&gt;Now for the fun part: Tdarr&amp;rsquo;s web dashboard is your command center for everything media transcoding. The interface is refreshingly straightforward, organized into four main sections that make sense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Libraries:&lt;/strong&gt; Tell Tdarr which folders to watch and process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transcode Settings:&lt;/strong&gt; Set up your conversion rules and choose plugins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nodes:&lt;/strong&gt; Monitor your transcoding workers (yes, you can run multiple)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs:&lt;/strong&gt; Watch the real-time activity feed and catch any hiccups&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="setting-up-your-first-library"&gt;Setting Up Your First Library
&lt;/h3&gt;&lt;p&gt;Time to point Tdarr at your media collection. This is where those bind mount mappings from earlier pay off:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the sidebar, click &lt;strong&gt;Libraries → Add Library&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give it a descriptive name like &amp;ldquo;Movies&amp;rdquo; or &amp;ldquo;TV Shows&amp;rdquo; (you&amp;rsquo;ll thank yourself later).&lt;/li&gt;
&lt;li&gt;Set the folder path to match what you mapped in your LXC (like &lt;code&gt;/media/Movies&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Set the library type to &amp;ldquo;Video.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tdarr immediately starts scanning that directory, cataloging every video file it finds. Depending on your collection size, this initial scan might take a few minutes, but you&amp;rsquo;ll see the progress in real time through the logs.&lt;/p&gt;
&lt;h2 id="step-7-defining-transcoding-rules"&gt;Step 7: Defining Transcoding Rules
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where Tdarr gets interesting. Rules are the brain of your operation: they decide which files get processed and how.&lt;/p&gt;
&lt;h3 id="creating-conditional-rules"&gt;Creating Conditional Rules
&lt;/h3&gt;&lt;p&gt;Every rule follows a simple IF → THEN logic. If a file meets certain conditions, Tdarr takes a specific action. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Condition:&lt;/strong&gt; File codec is H.264&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action:&lt;/strong&gt; Transcode to H.265&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setting this up is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;Plugins &amp;amp; Settings&lt;/strong&gt; menu.&lt;/li&gt;
&lt;li&gt;Add a new plugin chain for your library.&lt;/li&gt;
&lt;li&gt;Select relevant plugins (e.g., &amp;ldquo;Transcode using FFmpeg to H.265&amp;rdquo;).&lt;/li&gt;
&lt;li&gt;Adjust plugin parameters like bitrate or CRF (Constant Rate Factor) for quality control.&lt;/li&gt;
&lt;li&gt;Add pre-check plugins like &amp;ldquo;Skip file if already H.265&amp;rdquo; to avoid unnecessary work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once configured, Tdarr automatically queues any file that doesn&amp;rsquo;t meet your requirements for conversion. No more manual checking or guesswork.&lt;/p&gt;
&lt;h3 id="choosing-the-right-codec-and-container"&gt;Choosing the Right Codec and Container
&lt;/h3&gt;&lt;p&gt;Your codec and container choices should match where you actually watch your content. There&amp;rsquo;s no point optimizing for mobile if you&amp;rsquo;re streaming to a 65-inch TV.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Playback Environment&lt;/th&gt;
&lt;th&gt;Recommended Codec&lt;/th&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plex / Jellyfin&lt;/td&gt;
&lt;td&gt;H.265 (HEVC)&lt;/td&gt;
&lt;td&gt;MKV or MP4&lt;/td&gt;
&lt;td&gt;Widely supported, efficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct-to-TV&lt;/td&gt;
&lt;td&gt;H.264 (AVC)&lt;/td&gt;
&lt;td&gt;MP4&lt;/td&gt;
&lt;td&gt;Safest, but larger size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;H.265&lt;/td&gt;
&lt;td&gt;MP4&lt;/td&gt;
&lt;td&gt;Excellent compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archival Storage&lt;/td&gt;
&lt;td&gt;H.265 + 10-bit&lt;/td&gt;
&lt;td&gt;MKV&lt;/td&gt;
&lt;td&gt;High quality, smaller storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For most home server setups, &lt;strong&gt;H.265 MKV&lt;/strong&gt; hits the sweet spot: great compression, broad compatibility, and future-proof enough that you won&amp;rsquo;t be doing this again next year.&lt;/p&gt;
&lt;h3 id="a-practical-example-h264-to-h265-conversion"&gt;A Practical Example: H.264 to H.265 Conversion
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s how to set up a real transcoding rule. This example converts H.264 files to H.265 while maintaining quality:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Libraries&lt;/strong&gt; and select your library&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Transcode Options&lt;/strong&gt; → &lt;strong&gt;Plugin Stack&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Pre-Processing&lt;/strong&gt; section, add:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check if file is H264&lt;/strong&gt; (this filters which files to process)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Transcode&lt;/strong&gt; section, add:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transcode using FFmpeg - H265 QuickSync&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configure the plugin settings:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRF&lt;/strong&gt;: Set to 23 (good balance of quality/size)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preset&lt;/strong&gt;: Use &amp;ldquo;medium&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware Acceleration&lt;/strong&gt;: Enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Post-Processing&lt;/strong&gt;, add:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check file health&lt;/strong&gt; (validates the transcode worked)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Plugin Stack&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now Tdarr will automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scan your library for H.264 files&lt;/li&gt;
&lt;li&gt;Transcode them to H.265 using your iGPU&lt;/li&gt;
&lt;li&gt;Verify the output is valid&lt;/li&gt;
&lt;li&gt;Replace the original file (or keep both, depending on your settings)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is what I&amp;rsquo;m currently using:
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-flow_hu_e16e25fc55e44b2a.avif"&gt;&lt;img src="https://diymediaserver.com/post/setup-tdarr-automated-media-library-optimization/tdarr-flow_hu_15e55713e40b129f.webp" width="691" height="645" alt="My current flow" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Start with one folder to test before applying this to your entire library.&lt;/p&gt;
&lt;h3 id="monitoring-your-transcodes"&gt;Monitoring Your Transcodes
&lt;/h3&gt;&lt;p&gt;Once you&amp;rsquo;ve set up your rules, here&amp;rsquo;s how to track what&amp;rsquo;s happening:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check the Queue:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Transcode&lt;/strong&gt; → &lt;strong&gt;Queue&lt;/strong&gt; to see pending files&lt;/li&gt;
&lt;li&gt;The queue shows how many files are waiting and their estimated completion time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Watch Active Transcodes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click the &lt;strong&gt;Staging&lt;/strong&gt; tab to see files currently being processed&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ll see real-time stats: current FPS, progress percentage, and time remaining&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Review Completed Files:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Transcode&lt;/strong&gt; → &lt;strong&gt;History&lt;/strong&gt; tab shows finished jobs&lt;/li&gt;
&lt;li&gt;Look for the &amp;ldquo;Healthy&amp;rdquo; status to confirm successful transcodes&lt;/li&gt;
&lt;li&gt;Check the space savings column to see how much storage you&amp;rsquo;ve reclaimed&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Keep the logs open in a second browser tab (&lt;code&gt;sudo journalctl -u tdarr-node -f&lt;/code&gt; in terminal) to catch any errors early.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-8-verifying-output-quality"&gt;Step 8: Verifying Output Quality
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing about Tdarr: it&amp;rsquo;ll happily transcode your entire library without questioning whether the results actually look good. Quality control is entirely on you, which means you need to define what &amp;ldquo;acceptable&amp;rdquo; means for your setup.&lt;/p&gt;
&lt;h3 id="options-for-quality-verification"&gt;Options for Quality Verification
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visual spot-checking:&lt;/strong&gt; Grab a few random transcoded videos and watch them side-by-side with the originals. Your eyes are still the best quality detector.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bitrate inspection:&lt;/strong&gt; Fire up MediaInfo or similar tools to verify the output bitrate and codec specs match what you configured.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugin-based checks:&lt;/strong&gt; Some community plugins can automate verification by comparing resolution, color depth, or other metrics before marking a transcode as complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your output quality isn&amp;rsquo;t hitting the mark, start by tweaking your &lt;strong&gt;CRF value&lt;/strong&gt;. Remember: lower CRF means higher quality but larger file sizes. You can also adjust bitrate ceilings if you&amp;rsquo;re using constant bitrate encoding instead.&lt;/p&gt;
&lt;p&gt;Run the chain against a folder of twenty files before you point it at the whole library. If those look right at CRF 23, the rest will too, and if they don&amp;rsquo;t you&amp;rsquo;ve only burned twenty transcodes finding out.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DNMH4KQM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_b580_hu_fa4d16d6b98bd93a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_b580_hu_3cc7ffebfd0e4987.webp" width="600" height="607" alt="Sparkle Intel Arc B580 Titan" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Sparkle Intel Arc B580 Titan&lt;/strong&gt;
A midrange Battlemage card with 12GB GDDR6 and a tri-fan TORN 2.0 cooler that punches hard at 1080p–1440p, plus full-stack media engines (AV1/HEVC/H.264 encode/decode) that make it great for gaming rigs or Plex/Jellyfin transcoding. Outputs include HDMI 2.1 and DP 2.1 with support for up to four displays.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4oYTbOM" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-9-common-pitfalls--how-to-avoid-them"&gt;Step 9: Common Pitfalls &amp;amp; How to Avoid Them
&lt;/h2&gt;&lt;p&gt;Even with the best setup, Tdarr can throw you some curveballs. Here are the mistakes that trip up most people and how to dodge them:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing codecs&lt;/td&gt;
&lt;td&gt;FFmpeg plugins fail due to missing libraries&lt;/td&gt;
&lt;td&gt;Use official FFmpeg installations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow transcoding&lt;/td&gt;
&lt;td&gt;Low CPU or using software encoding&lt;/td&gt;
&lt;td&gt;Enable hardware acceleration if supported (e.g., Intel Quick Sync, NVIDIA NVENC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files not being processed&lt;/td&gt;
&lt;td&gt;Rule misconfiguration&lt;/td&gt;
&lt;td&gt;Double-check rule logic (&amp;ldquo;if already H.265&amp;rdquo; can skip intended files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node not connecting&lt;/td&gt;
&lt;td&gt;Firewall or wrong IP&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;Tdarr_ServerIP&lt;/code&gt; and matching port (8266)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No storage gain&lt;/td&gt;
&lt;td&gt;Using generous bitrate&lt;/td&gt;
&lt;td&gt;Lower CRF or target bitrate levels in transcoding profile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &amp;ldquo;files not being processed&amp;rdquo; issue deserves special attention. I&amp;rsquo;ve seen people scratch their heads for hours because they set up a rule that says &amp;ldquo;skip if already H.265&amp;rdquo; but then wonder why their H.265 files aren&amp;rsquo;t getting processed. The rule is doing exactly what you told it to do.&lt;/p&gt;
&lt;p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Test incrementally. Always run Tdarr on a small library first (like your &amp;ldquo;Short Clips&amp;rdquo; folder) before letting it loose on terabytes of data.
&lt;/div&gt;
&lt;/div&gt;
This is essential. Nothing quite compares to watching Tdarr churn through your entire movie collection only to discover you misconfigured something fundamental. Start small, verify everything works, then scale up.&lt;/p&gt;
&lt;h2 id="scaling-out-running-multiple-tdarr-nodes"&gt;Scaling Out: Running Multiple Tdarr Nodes
&lt;/h2&gt;&lt;p&gt;The single biggest reason people run Tdarr is also the part the basic setup skips: spreading transcodes across more than one machine. A single node is fine for a few hundred files, but if you&amp;rsquo;re re-encoding a multi-terabyte library, adding a second or third node turns a week-long job into an overnight one.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part that trips everyone up. The Tdarr &lt;strong&gt;server&lt;/strong&gt; only holds the queue and the database. The &lt;strong&gt;nodes&lt;/strong&gt; do the actual transcoding, and every node has to be able to read and write the files it&amp;rsquo;s working on. How you solve that is what separates a free multi-node setup from a paid one.&lt;/p&gt;
&lt;h3 id="how-tdarr-nodes-work"&gt;How Tdarr Nodes Work
&lt;/h3&gt;&lt;p&gt;Your server runs the web UI on port &lt;code&gt;8265&lt;/code&gt; and the job queue on port &lt;code&gt;8266&lt;/code&gt;. Each node connects back to the server on port &lt;code&gt;8266&lt;/code&gt;, pulls jobs from the queue, transcodes them, and reports back. You already built one node on the same box as your server in the steps above. A second node is the same &lt;code&gt;Tdarr_Node&lt;/code&gt; install on another machine, pointed at the server&amp;rsquo;s IP.&lt;/p&gt;
&lt;h3 id="option-1-mapped-nodes-free-shared-storage"&gt;Option 1: Mapped Nodes (Free, Shared Storage)
&lt;/h3&gt;&lt;p&gt;This is the standard, free approach, and it&amp;rsquo;s what I run. Every node mounts the &lt;strong&gt;same media and cache storage&lt;/strong&gt; over the network (NFS or SMB), so when a node grabs a job it can reach the file at a path it understands.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Put your media and Tdarr cache on shared storage&lt;/strong&gt;, typically an NFS export from your NAS. Both the server and every node mount it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Tdarr_Node on the second machine.&lt;/strong&gt; Repeat the node install from Step 3 (you only need the &lt;code&gt;Tdarr_Node&lt;/code&gt; package, not the server) and create its &lt;code&gt;Tdarr_Node_Config.json&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Point the node at your server.&lt;/strong&gt; Set &lt;code&gt;serverURL&lt;/code&gt; and &lt;code&gt;serverIP&lt;/code&gt; to your main Tdarr server&amp;rsquo;s address, leaving &lt;code&gt;serverPort&lt;/code&gt; at &lt;code&gt;8266&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Translate the paths if they differ.&lt;/strong&gt; If the second machine mounts the share at a different path than the server (say &lt;code&gt;/mnt/media&lt;/code&gt; instead of &lt;code&gt;/media&lt;/code&gt;), fill in the &lt;code&gt;pathTranslators&lt;/code&gt; block in the node config so Tdarr rewrites file paths between server and node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;pathTranslators&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/media&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;node&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/mnt/media&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start the node service&lt;/strong&gt; (the same &lt;code&gt;tdarr-node.service&lt;/code&gt; from earlier) and confirm the new node appears under &lt;strong&gt;Nodes&lt;/strong&gt; in the web UI.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If every machine mounts the share at the &lt;em&gt;same&lt;/em&gt; path, you can leave &lt;code&gt;pathTranslators&lt;/code&gt; empty, since the paths already match.&lt;/p&gt;
&lt;h3 id="option-2-unmapped-nodes-tdarr-pro"&gt;Option 2: Unmapped Nodes (Tdarr Pro)
&lt;/h3&gt;&lt;p&gt;If sharing storage isn&amp;rsquo;t practical, say a node in another location or a machine you can&amp;rsquo;t mount your NAS on, Tdarr&amp;rsquo;s &lt;strong&gt;unmapped nodes&lt;/strong&gt; (Tdarr 2.27.01+) download the working file to the node, transcode it, and upload it back automatically. No shared mounts required.&lt;/p&gt;
&lt;p&gt;The catch: on free Tdarr, unmapped nodes are capped at &lt;strong&gt;10MB per video file&lt;/strong&gt;, which is fine for testing and useless for real movies. &lt;strong&gt;Tdarr Pro&lt;/strong&gt; removes that limit and lets unmapped nodes process files of any size. Unmapped nodes also only run plugins that act on the working file itself (transcoding, remuxing); plugins that write side files like extracted subtitles won&amp;rsquo;t run on them.&lt;/p&gt;
&lt;p&gt;So here&amp;rsquo;s the honest decision. If you can share storage, mapped nodes are free and unlimited. Use them. If you genuinely can&amp;rsquo;t, Tdarr Pro is what makes unmapped nodes practical. Pricing is set on the &lt;a class="link" href="https://home.tdarr.io" target="_blank" rel="noopener"
&gt;official Tdarr site&lt;/a&gt; and changes from time to time, so check there for the current figure.&lt;/p&gt;
&lt;h2 id="troubleshooting-tdarr"&gt;Troubleshooting Tdarr
&lt;/h2&gt;&lt;p&gt;Even the best Tdarr setup will occasionally throw you a curveball. Here are the most common issues you&amp;rsquo;ll encounter and how to fix them without losing your sanity:&lt;/p&gt;
&lt;h3 id="problem-1-plugin-failed-messages"&gt;Problem 1: &amp;ldquo;Plugin failed&amp;rdquo; messages
&lt;/h3&gt;&lt;p&gt;When plugins refuse to cooperate, it&amp;rsquo;s usually one of these culprits:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Possible causes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Syntax errors in parameters&lt;/li&gt;
&lt;li&gt;Missing dependency&lt;/li&gt;
&lt;li&gt;FFmpeg unable to read specific container types&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update to the latest Tdarr first. Half the time, someone already fixed your problem.&lt;/li&gt;
&lt;li&gt;Double-check plugin settings against the FFmpeg syntax.&lt;/li&gt;
&lt;li&gt;Try another plugin (e.g., HandBrake-based instead of FFmpeg).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="problem-2-transcodes-crawling-along-at-glacial-speed"&gt;Problem 2: Transcodes crawling along at glacial speed
&lt;/h3&gt;&lt;p&gt;Nothing kills enthusiasm like watching a 2-hour movie take 8 hours to transcode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Possible causes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software-only encoding (your CPU is doing all the heavy lifting)&lt;/li&gt;
&lt;li&gt;High CRF (quality) or filters causing slowdowns&lt;/li&gt;
&lt;li&gt;Storage bottleneck from slow I/O&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure your hardware acceleration for your iGPU is working.&lt;/li&gt;
&lt;li&gt;Test with a higher CRF number or skip extra filters temporarily.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="problem-3-quality-looks-terrible"&gt;Problem 3: Quality looks terrible
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lower your CRF value (e.g., from 28 to 23). Lower numbers mean better quality.&lt;/li&gt;
&lt;li&gt;Use two-pass encoding for higher fidelity when quality matters most.&lt;/li&gt;
&lt;li&gt;Make sure you&amp;rsquo;re not transcoding already compressed H.265 files again unnecessarily. That&amp;rsquo;s like photocopying a photocopy.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="faqs-about-tdarr"&gt;FAQs About Tdarr
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What is the primary purpose of Tdarr?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Tdarr automates the tedious work of media management by transcoding your files according to rules you set up once and forget about. Think of it as your personal media librarian that standardizes formats, shrinks file sizes, and makes sure everything plays nicely across all your devices [Tdarr Docs].&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How does Tdarr handle distributed transcoding?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Tdarr supports &lt;strong&gt;multi-node distributed transcoding&lt;/strong&gt;, which is a fancy way of saying it can use multiple computers at once. Each node connects to your main Tdarr server and grabs jobs from the queue, processing them in parallel. This lets you throw more hardware at big libraries and see the transcoding finish before you retire.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can Tdarr be used with Jellyfin or Plex?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Absolutely. Tdarr and Plex work together like peanut butter and jelly. Use Tdarr to preprocess your entire library, ensuring every video file meets Plex&amp;rsquo;s supported codec and container requirements. When Plex streams your content, it can serve files directly without burning CPU cycles on real-time transcoding. Your server stays cool, your electricity bill stays reasonable, and your family stops complaining about buffering.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I verify the quality of transcoded files?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Here&amp;rsquo;s the thing: Tdarr doesn&amp;rsquo;t judge quality for you. It follows orders. You&amp;rsquo;ll need to use community verification plugins or do some manual spot-checking by playing back sample files and comparing metadata. A good practice is to transcode a few test files first and make sure you&amp;rsquo;re happy with the results before unleashing Tdarr on your entire collection.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What are common transcoding options?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;The most popular move is converting &lt;strong&gt;H.264 to H.265&lt;/strong&gt; (also called HEVC). This typically cuts file sizes by 30-50% while keeping the visual quality nearly identical. Other useful rules include converting audio tracks from DTS to AAC for better device compatibility, especially if you have mobile devices or streaming sticks that get picky about audio formats.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What ports does Tdarr use?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;By default Tdarr uses two ports: &lt;strong&gt;&lt;code&gt;8265&lt;/code&gt;&lt;/strong&gt; for the web interface (the dashboard you open in your browser) and &lt;strong&gt;&lt;code&gt;8266&lt;/code&gt;&lt;/strong&gt; for server–node communication. Worker nodes connect to the server on port &lt;code&gt;8266&lt;/code&gt;, so if you run nodes on other machines, that&amp;rsquo;s the port that needs to be reachable. Both are set in &lt;code&gt;Tdarr_Server_Config.json&lt;/code&gt; (&lt;code&gt;webUIPort&lt;/code&gt; and &lt;code&gt;serverPort&lt;/code&gt;) and can be changed if they clash with something else.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need Tdarr Pro, and what does it cost?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For a single node, or multiple nodes that share storage over NFS/SMB, you don&amp;rsquo;t need Tdarr Pro; the free version is fully featured for that. Pro&amp;rsquo;s main draw is &lt;strong&gt;unmapped nodes&lt;/strong&gt;: free Tdarr caps unmapped (auto-transferred) files at 10MB, and Pro removes that limit so you can transcode full-size files on nodes that can&amp;rsquo;t reach your shared storage. It also helps fund the project. Pricing is set on the &lt;a class="link" href="https://home.tdarr.io" target="_blank" rel="noopener"
&gt;official Tdarr site&lt;/a&gt; and changes from time to time, so check there for the current figure.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;Once Tdarr is running, it quietly works through your library on a schedule. The combination of rule-based automation and distributed transcoding means you set the codec, container, and CRF targets once and let the node chew through the queue. For Plex and Jellyfin owners with terabytes of H.264 sitting on spinning disks, the storage reclaim is real and the playback compatibility gets better at the same time.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been putting off optimizing your library, point Tdarr at a single folder this weekend before turning it loose on the whole collection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Only stripping junk audio and subtitle tracks, not re-encoding video?&lt;/strong&gt; Tdarr is more infrastructure than that one job needs. A lossless &lt;code&gt;mkvmerge&lt;/code&gt; remux does it in seconds. I tore Tdarr down for exactly this task and automated it with a SABnzbd hook and a cron sweep:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/featured_hu_1b3188e8f5db4034.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/featured_hu_294a5c070416e2c.webp" width="100" height="100" alt="Automated MKV Cleanup With mkvmerge, SABnzbd &amp;amp; Cron" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Automated MKV Cleanup With mkvmerge, SABnzbd &amp;amp; Cron&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/automated-mkv-cleanup-mkvmerge-sabnzbd-cron/"
class="backlink-button"
target="_self"
&gt;
Read
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="next-steps"&gt;Next Steps
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Read Tdarr&amp;rsquo;s &lt;a class="link" href="https://docs.tdarr.io" target="_blank" rel="noopener"
&gt;official documentation&lt;/a&gt; for advanced plugin scripting.&lt;/li&gt;
&lt;li&gt;Confirm hardware acceleration is active under load with &lt;code&gt;intel_gpu_top&lt;/code&gt; while a transcode runs.&lt;/li&gt;
&lt;li&gt;Build a second plugin stack for your TV shows that targets a different CRF or container than your movies.&lt;/li&gt;
&lt;li&gt;Join the Tdarr community forum to trade plugin recipes and troubleshooting tips.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>The Complete Guide to Securing SSH on Your Server</title><link>https://diymediaserver.com/post/complete-guide-securing-ssh-server/</link><pubDate>Wed, 17 Sep 2025 11:15:50 -0600</pubDate><guid>https://diymediaserver.com/post/complete-guide-securing-ssh-server/</guid><description>&lt;img src="https://diymediaserver.com/post/complete-guide-securing-ssh-server/featured_hu_18d8df80e1d7f85.webp" alt="Featured image of post The Complete Guide to Securing SSH on Your Server" /&gt;&lt;p&gt;If you run a Linux server, you probably use SSH to connect. Attackers know that too. The second port 22 is open to the internet, bots start hammering it with brute-force login attempts. I&amp;rsquo;ve watched fresh servers rack up thousands of attempts within hours of going online.&lt;/p&gt;
&lt;p&gt;Weak passwords, outdated cryptography, or a sloppy config can turn that background noise into a real problem. Data breach. Ransomware. Your box getting drafted into someone else&amp;rsquo;s botnet. All of that is on the table when SSH is left soft.&lt;/p&gt;
&lt;p&gt;So securing SSH is the first thing you do on any new server, not the last. A handful of config changes raises the bar high enough that most attackers move on to easier targets.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you&amp;rsquo;ll have a checklist you can run through on every server you touch. Don&amp;rsquo;t wait until you&amp;rsquo;re done reading. Apply each step as you go.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Ditch passwords for SSH keys, lock down root, tighten sshd_config, fix file permissions, deploy Fail2ban for brute-force protection, and watch your logs. If SSH must face the internet, put it behind a VPN.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0CW1BXZHK"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/glovary-N150_hu_fc897add6d45f778.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/glovary-N150_hu_d201079c859a028e.webp" width="600" height="477" alt="Glovary N150 Firewall Mini PC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Glovary N150 Firewall Mini PC&lt;/strong&gt;
This fanless N150 appliance runs pfSense or OPNsense, which gives you a clean place to gate SSH access for a homelab or media server. Six Intel i226-V 2.5GbE ports and hardware AES-NI keep routing fast under load, and the firewall rules are flexible enough to lock SSH down to specific source IPs or a VPN subnet. A solid pick if you want a dedicated edge device handling remote access instead of trusting your ISP router.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B02TcXOyP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A firewall appliance like that solves the network side of the problem. The rest of the work happens on the server itself, starting with how you authenticate.&lt;/p&gt;
&lt;h2 id="step-1-generate-a-secure-ssh-key-pair"&gt;Step 1: Generate a Secure SSH Key Pair
&lt;/h2&gt;&lt;p&gt;Password authentication over SSH is like leaving your house key under the doormat. It works, sure. Every bot on the internet is also happy to spend all day guessing your password. Key-based authentication is practically unbreakable when you use modern algorithms.&lt;/p&gt;
&lt;p&gt;To set up key-based authentication, you need to create the keys first. This works the same on Linux, macOS, and Windows (PowerShell or WSL).&lt;/p&gt;
&lt;p&gt;Run this on your client machine to create the key pair:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -t ed25519
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;ed25519&lt;/code&gt; algorithm generates keys that are both smaller and more secure than the older RSA standard. If you&amp;rsquo;re stuck on an older system that doesn&amp;rsquo;t support ed25519, fall back to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -t rsa -b &lt;span class="m"&gt;4096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates two files: a private key (which you guard with your life) and a public key (which you can share freely). By default, both keys land in your &lt;code&gt;.ssh&lt;/code&gt; folder (&lt;code&gt;~/.ssh/&lt;/code&gt; on Linux and macOS, &lt;code&gt;C:\Users\YourName\.ssh&lt;/code&gt; on Windows).&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Always set a passphrase on your private key. It&amp;rsquo;s your last line of defense if someone gets physical access to your laptop or workstation.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-2-deploy-your-public-key-to-the-server"&gt;Step 2: Deploy Your Public Key to the Server
&lt;/h2&gt;&lt;p&gt;Now you need to tell the server to trust your public key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatic method (the easy way):&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-copy-id user@server_address
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This does all the heavy lifting for you. It copies your public key and appends it to the right place on the server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manual method (when you want control):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the contents of your &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Append it to the server&amp;rsquo;s &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; file&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;rsquo;s the thing. Your first login still needs your password because the server hasn&amp;rsquo;t seen your key yet. Once the key is in place, you can disable password authentication entirely and never type that password again for SSH.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
Dual 10GbE SFP+ and dual 2.5GbE on a compact box makes the MS-A2 a strong fit for a homelab gateway or jump host. The networking headroom means you can run a hardened SSH server, a VPN endpoint, and a handful of containers without choking the link. Flexible storage and modern Ryzen silicon also give you room to spin up extra services as your lab grows.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Once your hardware is sorted and the key is on the server, the next job is to slam the SSH daemon&amp;rsquo;s defaults shut.&lt;/p&gt;
&lt;h2 id="step-3-secure-the-ssh-configuration"&gt;Step 3: Secure the SSH Configuration
&lt;/h2&gt;&lt;p&gt;The real power of SSH security lives in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;. This is where you harden the entry point and raise the bar high enough that attackers move on to softer targets.&lt;/p&gt;
&lt;p&gt;Open it with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/ssh/sshd_config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here are the most important changes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disable password authentication&lt;/strong&gt;&lt;br&gt;
This kills brute-force attacks dead.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PasswordAuthentication no
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Test your key-based login in a separate terminal session before making this change. Getting locked out of your own server is embarrassing and fixable, but it requires console access.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Disable root login&lt;/strong&gt;&lt;br&gt;
Never let root log in directly through SSH. Log in as your regular user and escalate with &lt;code&gt;sudo&lt;/code&gt; when needed.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;PermitRootLogin no
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Change the SSH port (optional)&lt;/strong&gt;&lt;br&gt;
Moving off port 22 cuts down on automated bot noise. It won&amp;rsquo;t stop a determined attacker, but it cleans up your logs.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Port 2222
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you change this, update your firewall rules at the same time. Don&amp;rsquo;t leave it for later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enforce modern encryption&lt;/strong&gt;&lt;br&gt;
Force SSH to use strong ciphers and message authentication codes. This prevents fallback to weaker algorithms.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Ciphers aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256,hmac-sha2-512
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Restrict user access&lt;/strong&gt;&lt;br&gt;
Limit SSH to specific users or groups instead of allowing every account on the box:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;AllowUsers youruser
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After making these changes, restart SSH to apply them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart ssh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On some distributions the service is named &lt;code&gt;sshd&lt;/code&gt; instead of &lt;code&gt;ssh&lt;/code&gt;. If the first command errors out, try &lt;code&gt;sudo systemctl restart sshd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Your SSH config is now meaningfully tighter than the defaults. These settings layer together, so an attacker who beats one still has to beat the others.&lt;/p&gt;
&lt;h2 id="step-4-set-correct-file-permissions"&gt;Step 4: Set Correct File Permissions
&lt;/h2&gt;&lt;p&gt;SSH has strong opinions about file permissions, and it&amp;rsquo;s not shy about telling you when they&amp;rsquo;re wrong. If your &lt;code&gt;.ssh&lt;/code&gt; directory or its files are too open, SSH will flat-out refuse to use them. Think of it as SSH protecting you from yourself.&lt;/p&gt;
&lt;p&gt;Here are the correct permissions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;700&lt;/span&gt; ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.ssh/id_ed25519
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;644&lt;/span&gt; ~/.ssh/id_ed25519.pub
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Breaking this down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;700&lt;/code&gt; on the &lt;code&gt;.ssh&lt;/code&gt; directory means only you can read, write, or enter it&lt;/li&gt;
&lt;li&gt;&lt;code&gt;600&lt;/code&gt; on private keys means only you can read or write them&lt;/li&gt;
&lt;li&gt;&lt;code&gt;644&lt;/code&gt; on the public key allows others to read it, which is fine because it&amp;rsquo;s public&lt;/li&gt;
&lt;li&gt;&lt;code&gt;600&lt;/code&gt; on &lt;code&gt;authorized_keys&lt;/code&gt; keeps it private to your account&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re seeing &amp;ldquo;bad owner or permissions on .ssh/config&amp;rdquo; errors, these settings will almost certainly fix it. SSH is picky about security, but once the permissions are right it works reliably.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
A Pi 4 with 4GB is a cheap, low-power way to practice SSH hardening on real hardware without putting a production server at risk. Spin up Ubuntu Server or Raspberry Pi OS, run through every step in this guide, and break it on purpose so you know what the failure modes look like. Once you&amp;rsquo;re confident, repeat the playbook on the box that matters.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;With the host hardened, you still need to think about what can reach it from the network.&lt;/p&gt;
&lt;h2 id="step-5-harden-network-access"&gt;Step 5: Harden Network Access
&lt;/h2&gt;&lt;p&gt;Your SSH service is now using keys and saner settings. Good. But if attackers can reach it from anywhere on the internet, you need more layers. Great lock on the front door, sure. Now stop putting the door on the street.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Firewalls&lt;/strong&gt;&lt;br&gt;
Limit SSH to specific source IPs where you can. This is your first line of defense. Example using UFW (Uncomplicated Firewall):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 203.0.113.10 to any port &lt;span class="m"&gt;2222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;VPN or Bastion Host&lt;/strong&gt;&lt;br&gt;
Put SSH behind a VPN. WireGuard or OpenVPN both work. This stops the whole internet from probing every machine you own. Only hosts on the VPN can see SSH at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fail2ban&lt;/strong&gt;&lt;br&gt;
Fail2ban scans logs and bans IPs that show malicious behavior, like repeated failed login attempts. Install it on Debian or Ubuntu with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install fail2ban
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then configure &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt; for the &lt;code&gt;sshd&lt;/code&gt; jail. Even with passwords disabled, Fail2ban still blocks port-scanning noise and keeps your logs cleaner. Think of it as a bouncer who remembers troublemakers and won&amp;rsquo;t let them back in.
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ A good Fail2Ban Starting Config for SSHD&lt;/summary&gt;
&lt;div class="collapse-content"&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;DEFAULT&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Whitelist: your trusted networks and admin IPs that should never be banned.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Replace the placeholders with your real IPs/subnets.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ignoreip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 127.0.0.1/8 ::1 192.168.1.0/24 10.0.0.0/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# How long to ban an offender (and enable incremental bans).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;bantime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 1h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bantime.increment &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bantime.factor &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bantime.maxtime &lt;span class="o"&gt;=&lt;/span&gt; 1w
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# How far back to count failures, and how many failures trigger a ban.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;findtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 10m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;maxretry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Use the systemd journal for log parsing.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; systemd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Use nftables if your distro uses it. Fall back to iptables if needed.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;banaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; nftables-multiport
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -----------------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# SSH JAIL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -----------------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;sshd&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; sshd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# If you changed the SSH port, reflect it here (e.g., port = 2222).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Mode “aggressive” catches more patterns (invalid users, many auth noise cases).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Requires fail2ban 0.11+ with newer sshd filter.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; aggressive
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Config Breakdown&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[DEFAULT]&lt;/code&gt; block: Sets global behavior all jails inherit.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ignoreip&lt;/code&gt; keeps your admin workstation/VPN/LAN from getting locked out during fat-finger moments.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bantime&lt;/code&gt;, &lt;code&gt;findtime&lt;/code&gt;, and &lt;code&gt;maxretry&lt;/code&gt; control the ban policy. Here: 5 bad tries within 10 minutes → ban. A 1-hour ban is long enough to stop bots but short enough to forgive honest mistakes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bantime.increment = true&lt;/code&gt; (+ factor, maxtime) makes repeat offenders stay banned longer (1h → 2h → 4h &amp;hellip; up to 1 week). This crushes persistent botnets without you micromanaging lists.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;backend = systemd&lt;/code&gt; reads from the journal instead of plain log files. It’s resilient to log rotation, works well on Debian/Ubuntu, and behaves nicely in containers/VMs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;banaction = nftables-multiport&lt;/code&gt; uses nftables rules to block offenders. If your host still uses iptables, switch to iptables-multiport.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[sshd]&lt;/code&gt; jail: The actual protection for SSH.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enabled = true&lt;/code&gt; turns it on.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter = sshd&lt;/code&gt; tells Fail2ban which regex set to use. It recognizes failed logins, invalid users, etc.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port = ssh&lt;/code&gt; binds bans to your SSH port. If you run SSH on a non-standard port, change it (e.g., 2222).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode = aggressive&lt;/code&gt; expands matches to catch more brute-force patterns and “invalid user” noise attackers use to enumerate accounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;&lt;/p&gt;
&lt;h2 id="step-6-test-and-verify"&gt;Step 6: Test and Verify
&lt;/h2&gt;&lt;p&gt;Now comes the moment of truth. Before you close that original SSH session (your safety net), make sure everything actually works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open a &lt;em&gt;new&lt;/em&gt; terminal window and test logging in with your key.&lt;/li&gt;
&lt;li&gt;Confirm you can&amp;rsquo;t log in as root.&lt;/li&gt;
&lt;li&gt;Confirm password login is refused.&lt;/li&gt;
&lt;li&gt;Check logs with:
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tail -f /var/log/auth.log
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;On systemd distributions without &lt;code&gt;auth.log&lt;/code&gt;, use &lt;code&gt;sudo journalctl -u ssh -f&lt;/code&gt; (or &lt;code&gt;-u sshd&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is where patience pays off. Only after confirming everything works should you close your original session. If something breaks, you still have that old session open to fix it. There&amp;rsquo;s nothing quite like the sinking feeling of being locked out of your own server because you skipped this step.&lt;/p&gt;
&lt;h2 id="step-7-ongoing-monitoring-and-auditing"&gt;Step 7: Ongoing Monitoring and Auditing
&lt;/h2&gt;&lt;p&gt;SSH hardening isn&amp;rsquo;t a one-and-done deal. Think of it like home security. You don&amp;rsquo;t install locks and then never check them again. Attackers adapt their methods, so your defenses need regular attention too.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what to keep an eye on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Review logs regularly&lt;/strong&gt; (&lt;code&gt;/var/log/auth.log&lt;/code&gt; or &lt;code&gt;journalctl -u ssh&lt;/code&gt;). Look for failed login attempts, especially repeated ones from the same IP addresses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit authorized keys&lt;/strong&gt; at least monthly. Remove old keys, particularly when team members leave or change roles. Stale keys are like forgotten spare house keys under the doormat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for anomalies&lt;/strong&gt; like new user accounts you didn&amp;rsquo;t create or SSH configuration changes you didn&amp;rsquo;t make. These can signal that someone is already in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System updates&lt;/strong&gt;: keep the box patched. OpenSSH gets security updates often, and missing one defeats everything else you&amp;rsquo;ve done. On Debian or Ubuntu, run &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade&lt;/code&gt; on a schedule, or use &lt;code&gt;unattended-upgrades&lt;/code&gt; for the security set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Set a calendar reminder to do this monthly. It takes maybe 10 minutes. Catching problems early beats dealing with a breach later.&lt;/p&gt;
&lt;h2 id="troubleshooting-common-ssh-problems"&gt;Troubleshooting Common SSH Problems
&lt;/h2&gt;&lt;p&gt;Even experienced admins lock themselves out occasionally. Here are the most common ways things go sideways and how to fix them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Locked out after disabling passwords&lt;/strong&gt;&lt;br&gt;
Classic mistake. Your SSH key wasn&amp;rsquo;t copied correctly before you disabled password authentication. Get into the server through your hosting provider&amp;rsquo;s web console or rescue environment, then re-enable &lt;code&gt;PasswordAuthentication yes&lt;/code&gt; in &lt;code&gt;sshd_config&lt;/code&gt; until you have your keys working properly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wrong file permissions&lt;/strong&gt;&lt;br&gt;
If you see errors like &lt;em&gt;&amp;ldquo;bad owner or permissions on .ssh/config&amp;rdquo;&lt;/em&gt;, your SSH files are readable by other users, which SSH treats as a security risk. Reset the permissions as shown in Step 4.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forgot to update firewall when changing port&lt;/strong&gt;&lt;br&gt;
Changed your SSH port to &lt;code&gt;Port 2222&lt;/code&gt; but forgot to open that port in the firewall? Your connection will hang, looking like a network issue. Always update firewall rules before changing the SSH port, not after.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trying to allow root login with keys&lt;/strong&gt;&lt;br&gt;
Even with &amp;ldquo;key-only&amp;rdquo; authentication, allowing direct root login creates unnecessary risk. If someone compromises your key, they have immediate root access. Stick with &lt;code&gt;PermitRootLogin no&lt;/code&gt; and use &lt;code&gt;sudo&lt;/code&gt; instead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VPN misconfiguration&lt;/strong&gt;&lt;br&gt;
If you&amp;rsquo;re routing SSH through a VPN, test access thoroughly in a non-production environment first. The added complexity can leave you stranded if you misconfigure it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0CLTNC6V6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="ASRock Mini-Desktop Computer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Mini-Desktop Computer&lt;/strong&gt;
This compact barebone system gives you a tidy place to run lightweight VMs or containers and isolate SSH services on their own host. Modern Intel CPUs and flexible storage make it a practical platform for a dedicated SSH gateway or jump box that sits between your homelab and the outside world. Quiet, efficient, and small enough that you&amp;rsquo;ll actually keep it powered on.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4kVe2jP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A dedicated jump host is a nice luxury, but it doesn&amp;rsquo;t change the questions readers ask most often. Here are the ones that keep coming up.&lt;/p&gt;
&lt;h2 id="faqs-secure-ssh-in-practice"&gt;FAQs: Secure SSH in Practice
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I generate a secure SSH key pair?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use &lt;code&gt;ssh-keygen -t ed25519&lt;/code&gt; for modern systems. If you&amp;rsquo;re stuck with older infrastructure, fall back to &lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt;. Ed25519 is faster, more secure, and generates smaller keys, but RSA with 4096 bits still does the job when needed.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why does SSH complain about bad owner or permissions?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;SSH is picky about file permissions because it has to be. Your &lt;code&gt;.ssh&lt;/code&gt; directory needs &lt;code&gt;700&lt;/code&gt; (owner read/write/execute only), private keys need &lt;code&gt;600&lt;/code&gt; (owner read/write only), and public keys need &lt;code&gt;644&lt;/code&gt; (owner read/write, others read). SSH refuses to work with loose permissions because anyone who can read your private key can impersonate you.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I change the SSH port safely?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and set &lt;code&gt;Port 2222&lt;/code&gt; (or whatever port you prefer). Here&amp;rsquo;s the critical part: update your firewall rules to allow the new port &lt;em&gt;before&lt;/em&gt; restarting SSH. Otherwise, you&amp;rsquo;ll lock yourself out and need console access to fix it. Test the new port works before closing your current session.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What are the most important `sshd_config` settings?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Start with these four: &lt;code&gt;PasswordAuthentication no&lt;/code&gt; (forces key-based auth), &lt;code&gt;PermitRootLogin no&lt;/code&gt; (eliminates the highest-value target), strong &lt;code&gt;Ciphers&lt;/code&gt; and &lt;code&gt;MACs&lt;/code&gt; (modern crypto only), and &lt;code&gt;AllowUsers&lt;/code&gt; or &lt;code&gt;AllowGroups&lt;/code&gt; (whitelist who can even attempt to connect). These settings alone will block most automated attacks.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is it safe to allow root login with SSH keys?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Best practice is &lt;strong&gt;no&lt;/strong&gt;, even with keys. Root access means game over if compromised, so why make it a direct target? Create a regular user account, give it sudo privileges, and SSH in as that user instead. One extra step eliminates the most obvious attack vector.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I still use RSA keys?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use Ed25519 when possible because it&amp;rsquo;s more secure and performs better. RSA with 4096 bits is still acceptable on systems that don&amp;rsquo;t support Ed25519, but avoid anything smaller than 2048 bits. If you&amp;rsquo;re generating new keys today, go with Ed25519 unless you have a specific reason not to.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What extra steps if SSH must face the internet?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Layer your defenses: restrict access by IP address when possible, install Fail2ban to block brute-force attempts, consider putting SSH behind a VPN or bastion host, and monitor your logs religiously. The internet is full of bots scanning for SSH servers, so assume you&amp;rsquo;re being probed constantly.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I set up Fail2ban for SSH?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Install Fail2ban through your package manager, then enable the &lt;code&gt;sshd&lt;/code&gt; jail in &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;. The default settings work well for most setups: they&amp;rsquo;ll ban IPs after a few failed attempts and gradually increase ban times for repeat offenders. Whitelist your own IP addresses first so a typo doesn&amp;rsquo;t lock you out.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What ciphers and MACs should I use?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Stick with modern algorithms like &lt;code&gt;aes256-ctr&lt;/code&gt; for encryption and &lt;code&gt;hmac-sha2-256&lt;/code&gt; for message authentication. Avoid anything with &amp;ldquo;md5&amp;rdquo; or &amp;ldquo;sha1&amp;rdquo; in the name, and definitely skip older ciphers like 3DES or Blowfish. When in doubt, let SSH negotiate the strongest common algorithm between client and server.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I recover access if I&amp;#39;m locked out?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use your hosting provider&amp;rsquo;s console access or rescue system to get back in. Fix whatever broke in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;, restart the SSH service, and test from another session before logging out. This is why you always test configuration changes before closing your current SSH session.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;If you take one thing from this guide, it&amp;rsquo;s this. SSH security depends less on any one trick and more on layers. Keys instead of passwords, no root logins, careful configuration, restrictive firewalls, monitoring, and patching all stack together to drastically lower your risk.&lt;/p&gt;
&lt;p&gt;Set aside time to secure SSH right after deploying any new server. It pays off later, whether you&amp;rsquo;re running a personal project or production infrastructure. Dealing with a compromised box is far worse than spending 30 minutes hardening SSH upfront.&lt;/p&gt;
&lt;p&gt;Next steps worth exploring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automating SSH hardening with configuration management tools like Ansible.&lt;/li&gt;
&lt;li&gt;Adding multi-factor authentication for SSH (Google Authenticator PAM or a hardware key like YubiKey).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run through this checklist on every new server. After a few rounds, the steps stop feeling like a chore and start feeling like muscle memory.&lt;/p&gt;
&lt;h2 id="other-sources"&gt;Other Sources
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://www.openssh.com" target="_blank" rel="noopener"
&gt;OpenSSH man pages and docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://help.ubuntu.com/community/SSH/OpenSSH/Configuring" target="_blank" rel="noopener"
&gt;Ubuntu Community Security Guide on SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/using-secure-shell_security-hardening" target="_blank" rel="noopener"
&gt;Red Hat Security Hardening for SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.fail2ban.org/wiki/index.php/Main_Page" target="_blank" rel="noopener"
&gt;Fail2ban Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys" target="_blank" rel="noopener"
&gt;DigitalOcean SSH Key Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://serverfault.com/questions/253313/ssh-permissions" target="_blank" rel="noopener"
&gt;Server Fault: SSH File Permission Pitfalls&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>How to Upgrade Debian 12 to 13 in Proxmox LXC Without CREDENTIALS Errors (2026)</title><link>https://diymediaserver.com/post/upgrade-debian-12-to-13-proxmox-lxc-243-credentials-fix/</link><pubDate>Sun, 14 Sep 2025 07:41:22 -0600</pubDate><guid>https://diymediaserver.com/post/upgrade-debian-12-to-13-proxmox-lxc-243-credentials-fix/</guid><description>&lt;img src="https://diymediaserver.com/post/upgrade-debian-12-to-13-proxmox-lxc-243-credentials-fix/featured_hu_5edc90a6bb94711b.webp" alt="Featured image of post How to Upgrade Debian 12 to 13 in Proxmox LXC Without CREDENTIALS Errors (2026)" /&gt;&lt;p&gt;Are you running Debian 12 (Bookworm) in an LXC container on Proxmox and want to upgrade to 13 (Trixie)? Should be straightforward, right? Well, if you&amp;rsquo;ve landed here, you&amp;rsquo;ve probably discovered that systemd has other plans.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the thing. systemd 256+ introduces stricter credential handling that doesn&amp;rsquo;t play nicely with unprivileged LXC containers. Try the upgrade and you&amp;rsquo;ll hit the dreaded &lt;code&gt;243/CREDENTIALS&lt;/code&gt; error that stops things dead.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how to work around it and upgrade your Debian LXC to Trixie without pulling your hair out.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Debian 13&amp;rsquo;s systemd 257 enables credential plumbing by default, which breaks unprivileged LXC containers with cryptic &lt;code&gt;243/CREDENTIALS&lt;/code&gt; errors. Install Debian&amp;rsquo;s &lt;code&gt;lxc.generator&lt;/code&gt; before you upgrade, reload systemd, then upgrade to Trixie. Leave the generator in place afterwards. Removing it hands the same failures straight back on the next reboot.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Proxmox VE&lt;/dt&gt;&lt;dd&gt;9.2.5&lt;/dd&gt;&lt;dt&gt;OS&lt;/dt&gt;&lt;dd&gt;Debian 13.6 (Trixie)&lt;/dd&gt;&lt;dt&gt;Kernel&lt;/dt&gt;&lt;dd&gt;7.0.14-6-pve&lt;/dd&gt;&lt;dt&gt;Container&lt;/dt&gt;&lt;dd&gt;unprivileged LXC, 4 cores / 8 GB&lt;/dd&gt;&lt;dt&gt;Systemd&lt;/dt&gt;&lt;dd&gt;257.13-1~deb13u1&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-08-04&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;I&amp;rsquo;m writing this inside one of those containers. It&amp;rsquo;s an unprivileged LXC on Proxmox VE 9.2.5 (kernel &lt;code&gt;7.0.14-6-pve&lt;/code&gt;) running Debian 13.6 with systemd &lt;code&gt;257.13-1~deb13u1&lt;/code&gt;. I dropped the generator into it on 14 September 2025 and never took it back out. As I type this, &lt;code&gt;systemctl is-system-running&lt;/code&gt; returns &lt;code&gt;running&lt;/code&gt; with zero failed units and userspace boots in 492 ms.&lt;/p&gt;
&lt;p&gt;Upgrading Debian containers inside Proxmox VE should be boring. Boring is why you run Debian. Change your apt sources, run the upgrade, reboot, get on with your life. That&amp;rsquo;s what the Reddit threads kept telling me when Debian 13 &amp;ldquo;Trixie&amp;rdquo; dropped. But if you&amp;rsquo;re running &lt;strong&gt;unprivileged and unnested LXCs on Proxmox 9&lt;/strong&gt;, you already know that&amp;rsquo;s not how it goes. The result? A blinking cursor, &lt;code&gt;status=243/CREDENTIALS&lt;/code&gt; failures, and crawling through logs that don&amp;rsquo;t want to work.&lt;/p&gt;
&lt;p&gt;So we&amp;rsquo;ll take it in order. Why the error happens. What breaks when you ignore it. How to get back into a container you&amp;rsquo;ve already broken. And why a &lt;em&gt;fresh&lt;/em&gt; Debian 13 container that was never upgraded at all greets you with the exact same mess.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
Up to a 16-core Ryzen 9 9955HX in something you can hide behind a monitor, with dual 10GbE SFP+ and dual 2.5GbE on board. The core count is what matters for this kind of work: enough headroom to run a stack of LXCs and still have something left over when one of them decides to rebuild its package cache mid-upgrade. Storage takes U.2 and full-length M.2 22110, so you&amp;rsquo;re not limited to whatever fits a laptop slot.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-upgrading-debian-12-to-13-breaks-on-proxmox-9"&gt;Why Upgrading Debian 12 to 13 Breaks on Proxmox 9
&lt;/h2&gt;&lt;p&gt;When you upgrade Debian 12 to 13 (Bookworm to Trixie), the culprit hiding in the shadows is &lt;strong&gt;systemd 256+&lt;/strong&gt;. Debian 13 ships systemd 257. That release enabled unit credentials (&lt;code&gt;LoadCredential&lt;/code&gt; and &lt;code&gt;ImportCredential&lt;/code&gt;) by default, which sounds harmless enough.&lt;/p&gt;
&lt;p&gt;On bare metal or regular VMs, it works fine. But inside Proxmox LXCs, especially unprivileged ones, this feature crashes headfirst into the container&amp;rsquo;s limited namespace access. Here&amp;rsquo;s the breakdown:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemd-sysctl&lt;/code&gt; (the service that applies kernel parameters) tries to load credentials&lt;/li&gt;
&lt;li&gt;LXC blocks the namespace operation because the container doesn&amp;rsquo;t have permission&lt;/li&gt;
&lt;li&gt;systemd throws a &lt;code&gt;status=243/CREDENTIALS&lt;/code&gt; error and gives up&lt;/li&gt;
&lt;li&gt;Other core services cascade into failure, from udev triggers to login shells&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most likely upgrade error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Job &lt;span class="k"&gt;for&lt;/span&gt; systemd-sysctl.service failed because the control process exited with error code.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;See &lt;span class="s2"&gt;&amp;#34;systemctl status systemd-sysctl.service&amp;#34;&lt;/span&gt; and &lt;span class="s2"&gt;&amp;#34;journalctl -xeu systemd-sysctl.service&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; details.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Processing trigger
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;systemctl status systemd-sysctl.service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x systemd-sysctl.service - Apply Kernel Variables
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/usr/lib/systemd/system/systemd-sysctl.service&lt;span class="p"&gt;;&lt;/span&gt; static&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Active: failed &lt;span class="o"&gt;(&lt;/span&gt;Result: exit-code&lt;span class="o"&gt;)&lt;/span&gt; since Sun 2025-09-14 05:59:21 MDT&lt;span class="p"&gt;;&lt;/span&gt; 48s ago
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Duration: 2h 59min 12.451s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Invocation: 63213f2f7514402f8cedd10155c7d065
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Docs: man:systemd-sysctl.service&lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; man:sysctl.d&lt;span class="o"&gt;(&lt;/span&gt;5&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Process: &lt;span class="m"&gt;17806&lt;/span&gt; &lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/systemd/systemd-sysctl &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exited, &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;243/CREDENTIALS&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Main PID: &lt;span class="m"&gt;17806&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exited, &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;243/CREDENTIALS&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Mem peak: 1.7M
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CPU: 5ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="what-that-error-means"&gt;What that error means
&lt;/h3&gt;&lt;p&gt;systemd is trying to pass secrets inside a container that has no infrastructure for handling them. Your services never start. The container hangs during boot. And debugging is a mess, because half the logging system went down with everything else.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;243/CREDENTIALS&lt;/code&gt; = &amp;ldquo;failed to set up the unit&amp;rsquo;s credentials&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="nesting-and-what-proxmox-actually-recommends"&gt;Nesting, and what Proxmox actually recommends
&lt;/h3&gt;&lt;p&gt;The other way out of this is &lt;strong&gt;nesting&lt;/strong&gt;. I want to be honest with you about it, because the original version of this post was harder on nesting than the facts support.&lt;/p&gt;
&lt;p&gt;Proxmox&amp;rsquo;s own position is that modern systemd needs it. Their staff have said so plainly on the forums: systemd versions 242 and newer want nesting so they can create the Linux namespaces used to isolate services. That requirement has since been written into the &lt;code&gt;pve-container&lt;/code&gt; package itself, where the nesting feature description now ends with the line &amp;ldquo;That is also required by systemd to isolate services.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The security tradeoff is real, but it&amp;rsquo;s narrower than it sounds. On an unprivileged container, nesting exposes the host&amp;rsquo;s procfs and sysfs contents to the guest. The Proxmox documentation describes the feature as &amp;ldquo;best used with unprivileged containers with additional id mapping,&amp;rdquo; which is another way of saying it&amp;rsquo;s the supported configuration rather than a workaround.&lt;/p&gt;
&lt;p&gt;Privileged plus nesting is the combination that should worry you. Proxmox staff describe that pairing as &amp;ldquo;essentially uncontained.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;For the record, every container in my own fleet now runs unprivileged with &lt;code&gt;nesting=1&lt;/code&gt;. The generator approach in this post is what you reach for when you specifically want nesting turned off, or when you&amp;rsquo;re upgrading a container in place and would rather not change its feature flags mid-migration. Both paths work. Pick the one whose tradeoff you&amp;rsquo;d rather own.&lt;/p&gt;
&lt;h2 id="warn-systemd-257-detected-you-may-need-to-enable-nesting"&gt;&amp;ldquo;WARN: systemd 257 detected. You may need to enable nesting&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;If you got here by pasting that exact string into a search box, this section is for you.&lt;/p&gt;
&lt;p&gt;Proxmox added the warning in &lt;code&gt;pve-container&lt;/code&gt; 6.0.19. Start a container running a modern systemd and it tells you up front that nesting may be required. You&amp;rsquo;ll see it on &lt;code&gt;pct start&lt;/code&gt; or in the task log:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WARN: systemd &lt;span class="m"&gt;257&lt;/span&gt; detected. You may need to &lt;span class="nb"&gt;enable&lt;/span&gt; nesting.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two things worth knowing.&lt;/p&gt;
&lt;p&gt;First, the number is whatever systemd version your container ships. People see 256, 257 and 258 depending on the distribution inside. Debian 13 reports 257. Same cause, same fix. Guidance written about one number applies to the rest.&lt;/p&gt;
&lt;p&gt;Second, it&amp;rsquo;s a warning. Nothing more. The container will still start. What happens after that depends on whether anything inside it needs the credential machinery, and on Debian 13 with nesting off and no generator installed, plenty does. The container comes up &lt;strong&gt;degraded&lt;/strong&gt; with a stack of units failing &lt;code&gt;243/CREDENTIALS&lt;/code&gt;, &lt;code&gt;systemd-udev-load-credentials.service&lt;/code&gt; among them, and the Proxmox web console shows a black screen where the login prompt should be.&lt;/p&gt;
&lt;p&gt;You have two ways to make the warning stop mattering:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Option A: take Proxmox&amp;#39;s advice and enable nesting (run on the host)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;CTID&amp;gt; --features &lt;span class="nv"&gt;nesting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct reboot &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Option B: install the generator inside the container and leave nesting off&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# (full walkthrough below)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The warning itself doesn&amp;rsquo;t disappear with Option B, because Proxmox emits it from the systemd version it detects, without checking whether the container is healthy. Confirm with &lt;code&gt;systemctl is-system-running&lt;/code&gt; inside the container instead of trusting the absence of a warning.&lt;/p&gt;
&lt;h2 id="what-breaks-if-you-dont-fix-it"&gt;What breaks if you don&amp;rsquo;t fix it
&lt;/h2&gt;&lt;p&gt;Try to upgrade to Debian 13 (Trixie) without handling the systemd changes first, and your LXC container becomes a flashing cursor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The container won&amp;rsquo;t boot cleanly. It hangs there, mocking your weekend plans.&lt;/li&gt;
&lt;li&gt;Critical systemd units like &lt;code&gt;systemd-sysctl&lt;/code&gt;, &lt;code&gt;systemd-udev-trigger&lt;/code&gt;, &lt;code&gt;systemd-udev-load-credentials&lt;/code&gt; and terminal login will fail consistently. One report against the community-scripts project counted 19 failed units on a fresh Debian 13 container that had never been upgraded at all.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;journald&lt;/code&gt; may crash, which means you lose the diagnostic logs you desperately need to figure out what went wrong.&lt;/li&gt;
&lt;li&gt;The Proxmox noVNC console shows nothing at all, because &lt;code&gt;agetty&lt;/code&gt; is one of the casualties.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point catches people out, and it&amp;rsquo;s also the thing that saves you. The console being dead doesn&amp;rsquo;t mean the container is unreachable. See the recovery section below.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CLTNC6V6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="ASRock Mini-Desktop Computer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Mini-Desktop Computer&lt;/strong&gt;
A 1.92L barebone that takes any 65W LGA1700 chip from 12th through 14th Gen, so you pick the CPU rather than accepting whatever a vendor soldered down. Two SO-DIMM slots reach 64GB, and you get a Gen5 x4 M.2, a Gen4 x4 M.2 and two 2.5-inch bays. It&amp;rsquo;s a barebone, so budget for the CPU, RAM, cooler and drives on top of the sticker price.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4kVe2jP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-fix-lxcgenerator-for-proxmox-lxc-templates"&gt;The Fix: lxc.generator for Proxmox LXC Templates
&lt;/h2&gt;&lt;p&gt;Debian includes a small but clever utility called &lt;strong&gt;lxc.generator&lt;/strong&gt; (part of the distrobuilder package) that solves this problem. It runs early in the systemd boot sequence and automatically patches unit files to make them container-friendly &lt;a class="link" href="https://sources.debian.org/src/distrobuilder/3.2-2/distrobuilder/lxc.generator/" target="_blank" rel="noopener"
&gt;Debian Sources&lt;/a&gt;. Here&amp;rsquo;s what it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Detects when the system is running inside an LXC container or an unprivileged environment&lt;/li&gt;
&lt;li&gt;Strips problematic flags like &lt;code&gt;LoadCredential=&lt;/code&gt; and &lt;code&gt;ImportCredential=&lt;/code&gt; that cause the 243/CREDENTIALS errors&lt;/li&gt;
&lt;li&gt;Relaxes security hardening settings that don&amp;rsquo;t work properly in containers&lt;/li&gt;
&lt;li&gt;Masks services that would otherwise crash during container startup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The generator creates temporary drop-in files under &lt;code&gt;/run/systemd/&lt;/code&gt; rather than permanently modifying anything on disk. Nothing on your root filesystem changes, and the drop-ins are rebuilt from scratch on every boot.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Tested on &lt;strong&gt;Proxmox VE 9.x&lt;/strong&gt; with &lt;strong&gt;unprivileged&lt;/strong&gt; Debian LXCs. It should also help in LXD or Incus, but I have not tested there.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="what-the-generator-actually-changes"&gt;What the generator actually changes
&lt;/h3&gt;&lt;p&gt;The original version of this post claimed the generator kept your security posture intact with no compromises. That wasn&amp;rsquo;t quite right, and anyone who read the script would have caught me out. Here&amp;rsquo;s the drop-in it writes to &lt;code&gt;/run/systemd/system/service.d/zzz-lxc-service.conf&lt;/code&gt; on a Debian 13 container, copied from the container this post was written in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProcSubset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectProc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectControlGroups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ProtectKernelTunables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;LoadCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PrivateNetwork&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ImportCredential&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s a global drop-in. It applies to &lt;strong&gt;every service in the container&lt;/strong&gt;, and it turns off a meaningful chunk of systemd&amp;rsquo;s per-service sandboxing to do it. &lt;code&gt;NoNewPrivileges=no&lt;/code&gt; in particular is not nothing.&lt;/p&gt;
&lt;p&gt;So the honest framing is that both options cost you something. Nesting widens what the guest can see of the host. The generator widens what services inside the guest can do to each other. Neither is free, and which one you prefer depends on which boundary you actually care about.&lt;/p&gt;
&lt;p&gt;It also writes a per-unit override for the service that started this whole mess:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This file was created by distrobuilder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-/usr/lib/systemd/systemd-sysctl&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The leading &lt;code&gt;-&lt;/code&gt; tells systemd to tolerate a non-zero exit from &lt;code&gt;systemd-sysctl&lt;/code&gt; instead of failing the unit, which is why the container stops cascading into failure.&lt;/p&gt;
&lt;h3 id="1-patch-each-debian-12-container-before-upgrading"&gt;1) Patch each Debian 12 container &lt;em&gt;before&lt;/em&gt; upgrading
&lt;/h3&gt;&lt;p&gt;Before you upgrade Debian 12 to 13, install a small fix that prevents systemd from breaking during the transition. Enter your Debian 12 (Bookworm) LXC container and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /etc/systemd/system-generators
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://sources.debian.org/data/main/d/distrobuilder/3.2-2/distrobuilder/lxc.generator &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/systemd/system-generators/lxc &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod &lt;span class="m"&gt;0755&lt;/span&gt; /etc/systemd/system-generators/lxc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
The generator tells systemd to disable the credential features that cause the 243/CREDENTIALS error. Install it while the container is still healthy, so the fix is already in place when the new systemd arrives.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Now prove it took effect before you go any further. A &lt;code&gt;daemon-reload&lt;/code&gt; runs the generator, so the drop-in should exist immediately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /run/systemd/system/service.d/zzz-lxc-service.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that file is missing, the generator didn&amp;rsquo;t run. Check that it&amp;rsquo;s executable (&lt;code&gt;ls -l /etc/systemd/system-generators/lxc&lt;/code&gt; should show &lt;code&gt;0755&lt;/code&gt;) and that the download wasn&amp;rsquo;t truncated (it&amp;rsquo;s roughly 7 KB). Fix that before upgrading, because this is the last comfortable moment to do it.&lt;/p&gt;
&lt;h3 id="2-upgrade-to-debian-13-trixie"&gt;2) Upgrade to Debian 13 (Trixie)
&lt;/h3&gt;&lt;p&gt;Still inside your LXC container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Make sure Bookworm is up to date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Update every sources file to Trixie, old-style and deb822 alike&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; f in /etc/apt/sources.list &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/apt/sources.list.d/*.list &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/apt/sources.list.d/*.sources&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo sed -i &lt;span class="s1"&gt;&amp;#39;s/bookworm/trixie/g&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Upgrade across versions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt dist-upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Clean up junk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt autoremove -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt autoclean -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Modernize apt format (optional but recommended)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt modernize-sources &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Reload systemd configs and reboot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Earlier versions of this post rewrote only &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;. That misses two common cases: repositories dropped into &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt;, and containers that already run the deb822 &lt;code&gt;.sources&lt;/code&gt; format, where &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; may not exist at all. The loop above covers all three paths. Run &lt;code&gt;apt update&lt;/code&gt; and read the output before you commit to &lt;code&gt;dist-upgrade&lt;/code&gt;.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="3-verify"&gt;3) Verify
&lt;/h3&gt;&lt;p&gt;The upgrade takes a few minutes, depending on how big the container is and how fast your mirror is. When it comes back, check three things:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /etc/os-release
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl is-system-running
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --failed
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PRETTY_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Debian GNU/Linux 13 (trixie)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VERSION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;13&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;13 (trixie)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VERSION_CODENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trixie
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DEBIAN_VERSION_FULL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;13.6
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;See &amp;ldquo;trixie&amp;rdquo; in the output? Good. Now check the second and third commands: &lt;code&gt;is-system-running&lt;/code&gt; should print &lt;code&gt;running&lt;/code&gt;, and &lt;code&gt;systemctl --failed&lt;/code&gt; should list nothing. A &lt;code&gt;degraded&lt;/code&gt; result with &lt;code&gt;243/CREDENTIALS&lt;/code&gt; units in the list means the generator isn&amp;rsquo;t doing its job, so go back and check the drop-in from step 1.&lt;/p&gt;
&lt;p&gt;Debian 13.6 has been the current point release since 11 July 2026. Yours will differ depending on when you read this, and a higher &lt;code&gt;DEBIAN_VERSION_FULL&lt;/code&gt; is nothing to worry about.&lt;/p&gt;
&lt;h3 id="4-leave-the-generator-in-place"&gt;4) Leave the generator in place
&lt;/h3&gt;&lt;p&gt;This is the one instruction I&amp;rsquo;ve reversed since first publishing this post, and it&amp;rsquo;s worth explaining why.&lt;/p&gt;
&lt;p&gt;The original advice was to delete &lt;code&gt;/etc/systemd/system-generators/lxc&lt;/code&gt; once the upgrade completed, on the theory that it was a temporary bridge. It isn&amp;rsquo;t. The credential behaviour that broke your container during the upgrade is the ordinary behaviour of systemd 257 on Debian 13, so removing the generator hands you the same failures back on the next reboot. That&amp;rsquo;s borne out by everyone who creates a &lt;em&gt;fresh&lt;/em&gt; Debian 13 container with nesting off and watches it come up degraded without ever having run an upgrade at all.&lt;/p&gt;
&lt;p&gt;The container I&amp;rsquo;m writing this in has had the generator installed since 14 September 2025. It&amp;rsquo;s been through the 13.1 through 13.6 point releases and every reboot along the way, the most recent on 24 July 2026, and it still reports zero failed units. Leave it alone.&lt;/p&gt;
&lt;p&gt;If you do want it gone, enable nesting first and reboot, then confirm the container still reports &lt;code&gt;running&lt;/code&gt; before you delete anything.&lt;/p&gt;
&lt;h2 id="already-upgraded-and-now-the-container-wont-boot"&gt;Already upgraded and now the container won&amp;rsquo;t boot
&lt;/h2&gt;&lt;p&gt;If you found this post &lt;em&gt;after&lt;/em&gt; running the upgrade, you&amp;rsquo;re not stuck, and you almost certainly don&amp;rsquo;t need to restore from backup.&lt;/p&gt;
&lt;p&gt;The critical detail: &lt;strong&gt;the Proxmox console dying doesn&amp;rsquo;t mean the container is unreachable.&lt;/strong&gt; &lt;code&gt;agetty&lt;/code&gt; fails on credentials, which kills the noVNC console, but &lt;code&gt;pct enter&lt;/code&gt; attaches through a different path and keeps working. From the Proxmox host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct enter &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once you&amp;rsquo;re in, install the generator exactly as in step 1, then reload and reboot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /etc/systemd/system-generators
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://sources.debian.org/data/main/d/distrobuilder/3.2-2/distrobuilder/lxc.generator &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;gt; /etc/systemd/system-generators/lxc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0755&lt;/span&gt; /etc/systemd/system-generators/lxc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /run/systemd/system/service.d/zzz-lxc-service.conf &lt;span class="c1"&gt;# proof it ran&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# back on the host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct reboot &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The container should come back with a working console. If &lt;code&gt;pct enter&lt;/code&gt; also fails, or networking never came up so you can&amp;rsquo;t reach &lt;code&gt;sources.debian.org&lt;/code&gt;, enable nesting from the host as the fast way back to a shell:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;CTID&amp;gt; --features &lt;span class="nv"&gt;nesting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct reboot &amp;lt;CTID&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That gets you a bootable container immediately. You can decide afterwards whether to install the generator and turn nesting back off, or leave it on and call it done.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel® Core™ i5-14500 14th Generation Desktop Processor&lt;/strong&gt;
14 cores (6 performance plus 8 efficiency) and 20 threads turboing to 5.0 GHz, with UHD 770 graphics carrying the Quick Sync engine. That last part is why this chip keeps turning up in homelab builds: Jellyfin and Plex get hardware transcoding without a discrete GPU taking up a slot. The thread count suits a Proxmox host running a dozen containers that spend most of their lives idle.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="starting-fresh-creating-a-debian-13-lxc-from-a-template"&gt;Starting fresh: creating a Debian 13 LXC from a template
&lt;/h2&gt;&lt;p&gt;Plenty of people arrive here not because an upgrade broke, but because they tried to create a new Debian 13 container and hit a wall. The credential problem is identical. The path to it is different.&lt;/p&gt;
&lt;p&gt;Grab the template from the host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveam update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveam available --section system &lt;span class="p"&gt;|&lt;/span&gt; grep debian-13
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pveam download &lt;span class="nb"&gt;local&lt;/span&gt; &amp;lt;template-filename-from-the-list-above&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve deliberately not hardcoded the filename here, because it carries the point release and changes every couple of months. Read it off the &lt;code&gt;pveam available&lt;/code&gt; output rather than copying a stale one out of a blog post.&lt;/p&gt;
&lt;p&gt;Once the container is created, it runs straight into the same systemd 257 credential behaviour that broke the upgrade path. A fresh Debian 13 container with nesting disabled comes up degraded, and the fix is the same generator install from step 1. This surprises people who assume the problem was caused by the upgrade process. It wasn&amp;rsquo;t. The upgrade was only when they first met it.&lt;/p&gt;
&lt;h3 id="unsupported-debian-version-when-creating-the-container"&gt;&amp;ldquo;unsupported debian version&amp;rdquo; when creating the container
&lt;/h3&gt;&lt;p&gt;There&amp;rsquo;s a second, unrelated error that hits Debian 13 containers, and it looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;TASK ERROR: unable to create CT &lt;span class="m"&gt;200&lt;/span&gt; - unsupported debian version &lt;span class="s1"&gt;&amp;#39;13.1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Some tooling surfaces it as a complaint that &lt;code&gt;pct&lt;/code&gt; reported an unsupported version and the LXC stack might be too old for the template. Same root cause either way: &lt;code&gt;/usr/share/perl5/PVE/LXC/Setup/Debian.pm&lt;/code&gt; on the host carries a hardcoded upper bound on the Debian version it will accept, and a template newer than that bound gets rejected before the container is ever created.&lt;/p&gt;
&lt;p&gt;The fix is to update Proxmox. Thomas Lamprecht confirmed the version check was reworked in &lt;code&gt;pve-container&lt;/code&gt; 6.0.10 for Proxmox VE 9 and 5.3.1 for Proxmox VE 8, both on the &lt;code&gt;pve-no-subscription&lt;/code&gt; repository. The refactor also stops the same thing recurring with each new point release, which is what made 13.0 work and 13.1 fail.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt install pve-container
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you genuinely can&amp;rsquo;t update the host right now, the interim workaround is to edit line 39 of &lt;code&gt;/usr/share/perl5/PVE/LXC/Setup/Debian.pm&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-perl" data-lang="perl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;unsupported debian version &amp;#39;$version&amp;#39;\n&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# after&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;die&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;unsupported debian version &amp;#39;$version&amp;#39;\n&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Restart the host afterwards. Treat this as a stopgap. A package update will overwrite the file, which is fine, because by then you won&amp;rsquo;t need the edit.&lt;/p&gt;
&lt;h2 id="alternative-options-pick-your-poison"&gt;Alternative Options: Pick Your Poison
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;What it costs you&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Use when…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable nesting (unprivileged)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Host procfs and sysfs contents visible to the guest&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;You want the configuration Proxmox documents and supports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use &lt;code&gt;lxc.generator&lt;/code&gt; (this post)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;systemd&amp;rsquo;s per-service sandboxing relaxed container-wide&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;You want nesting to stay off, or you&amp;rsquo;re upgrading in place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flip to privileged LXC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UID mapping isolation gone, much larger blast radius&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;You need kernel options that won&amp;rsquo;t work unprivileged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Migrate to VM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real overhead and resource cost&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;You want zero container weirdness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The top two are both defensible and I run the first one across my fleet. Avoid combining privileged with nesting, which Proxmox staff describe as essentially uncontained.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What does `status=243/CREDENTIALS` mean, exactly?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;It&amp;rsquo;s a systemd error code that translates to &amp;ldquo;failed to set up credentials.&amp;rdquo; Starting with systemd 256, the system expects to mount sensitive files like API tokens into service units through tmpfs-backed namespaces. The problem? Unprivileged LXC containers don&amp;rsquo;t have the kernel options to support this feature, so services that rely on it fail to start.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why does Proxmox tell me to enable nesting if this post shows a way around it?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Because nesting is the configuration Proxmox supports for modern systemd, and their warning reflects that. The generator is a legitimate alternative when you want nesting off, but it isn&amp;rsquo;t the officially blessed path. Both work. The generator trades systemd&amp;rsquo;s in-container sandboxing for the host visibility that nesting would give away.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does the systemd version number in the warning matter?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. Whether it says 256, 257 or 258 depends on which distribution release is inside the container. Debian 13 reports 257. The cause is the same credential handling in all of them and the fix doesn&amp;rsquo;t change, so guidance written against one number applies to the rest.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Could I enable nesting and forget about it?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, and that&amp;rsquo;s what I do on my own fleet. Nesting exposes more of the host&amp;rsquo;s &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; to the container, which is a real cost, but on an unprivileged container with id mapping it&amp;rsquo;s the arrangement Proxmox recommends. Keep it away from privileged containers, where the combination removes most of the isolation.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is `lxc.generator` safe to leave on long-term?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, and you should. It only creates ephemeral drop-in files under &lt;code&gt;/run/systemd/&lt;/code&gt;, rebuilt on every boot. Removing it hands back the same 243/CREDENTIALS failures on the next reboot, because Debian 13&amp;rsquo;s credential behaviour doesn&amp;rsquo;t go away after the upgrade. Mine has run continuously since September 2025.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ My container won&amp;#39;t boot and the console is black. Have I lost it?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Almost certainly not. The dead console is &lt;code&gt;agetty&lt;/code&gt; failing on credentials, not the container being down. Run &lt;code&gt;pct enter &amp;lt;CTID&amp;gt;&lt;/code&gt; from the Proxmox host, which uses a different path and usually still works, then install the generator and reboot. Failing that, enable nesting from the host to get a shell back.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use this on LXD instead of Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;In theory, yes. The &lt;code&gt;lxc.generator&lt;/code&gt; is designed to work with LXC containers, not only Proxmox. But I&amp;rsquo;ve only run these steps on Proxmox VE 9 with unprivileged containers. On LXD or Incus the concepts should carry over fine, and you&amp;rsquo;d be the one testing that.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need to replace my `sources.list` with deb822 format?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Not required. Your existing old-style sources keep working fine. Debian is nudging everyone toward the newer deb822 format though, and &lt;code&gt;apt modernize-sources&lt;/code&gt; handles the conversion for you if you decide you want it. There&amp;rsquo;s no hurry either way.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Upgrading an unprivileged Debian container on Proxmox shouldn&amp;rsquo;t require a vocabulary lesson in creative profanity. The culprit is systemd&amp;rsquo;s credential defaults in 256 and later, which Debian 13 inherits at version 257.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve got two honest ways through it. Enable nesting, which is what Proxmox documents and what I run across my own containers. Or install &lt;strong&gt;lxc.generator&lt;/strong&gt;, keep nesting off, and accept that systemd&amp;rsquo;s per-service hardening gets relaxed inside the container instead. Install it, reload, upgrade, reboot, and then leave it in place. That last part is the correction I most wanted to make to this post: the generator is not scaffolding you remove once the building is up.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re already staring at a container that won&amp;rsquo;t boot, &lt;code&gt;pct enter&lt;/code&gt; is almost always still open to you. And if you&amp;rsquo;re creating a fresh Debian 13 container rather than upgrading one, expect exactly the same credential failures, plus a possible &lt;code&gt;unsupported debian version&lt;/code&gt; error that a &lt;code&gt;pve-container&lt;/code&gt; update clears.&lt;/p&gt;
&lt;p&gt;LXC template upgrades only &lt;em&gt;look&lt;/em&gt; straightforward until systemd changes the rules mid-game. So when a container comes up degraded after some future point release, run &lt;code&gt;ls /run/systemd/system/service.d/&lt;/code&gt; before you go digging through logs. One command tells you whether the generator is still doing its job.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
Quad-core ARM, 4GB of RAM, Gigabit Ethernet and dual-band Wi-Fi, sipping a few watts. It won&amp;rsquo;t run your media server and isn&amp;rsquo;t meant to. Think of it as the machine you keep around for Pi-hole or Home Assistant, and for the throwaway Debian install you can break on purpose to see what a failed upgrade looks like before you try it on something you care about.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="sources"&gt;Sources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://sources.debian.org/src/distrobuilder/3.2-2/distrobuilder/lxc.generator/" target="_blank" rel="noopener"
&gt;Debian Sources: lxc.generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://bugs.launchpad.net/bugs/2046486" target="_blank" rel="noopener"
&gt;Launchpad bug report discussing credential errors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/lxc/distrobuilder/releases" target="_blank" rel="noopener"
&gt;LXC Distrobuilder Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://forum.proxmox.com/threads/lxc-debian-13-with-nesting-disabled-no-console.173418/" target="_blank" rel="noopener"
&gt;Proxmox forum: LXC Debian 13 with nesting disabled, no console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://lists.proxmox.com/pipermail/pve-devel/2025-October/076160.html" target="_blank" rel="noopener"
&gt;pve-devel: document that systemd requires nesting (bug #6897)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://forum.proxmox.com/threads/debian-13-1-lxc-template-fails-to-create-start-fix.171435/" target="_blank" rel="noopener"
&gt;Proxmox forum: Debian 13.1 LXC template fails to create/start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/community-scripts/ProxmoxVE/issues/11204" target="_blank" rel="noopener"
&gt;community-scripts/ProxmoxVE #11204: Debian 13 LXC starts degraded when nesting is disabled&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://www.debian.org/releases/trixie/" target="_blank" rel="noopener"
&gt;Debian 13 point releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>NFS vs SMB: Which Is Faster for Home Media Servers?</title><link>https://diymediaserver.com/post/nfs-smb/</link><pubDate>Thu, 11 Sep 2025 06:54:35 -0600</pubDate><guid>https://diymediaserver.com/post/nfs-smb/</guid><description>&lt;img src="https://diymediaserver.com/post/nfs-smb/featured_hu_8e6fc211c2173888.webp" alt="Featured image of post NFS vs SMB: Which Is Faster for Home Media Servers?" /&gt;&lt;p&gt;I used to think choosing between NFS and SMB was like picking between Coke and Pepsi: a purely personal preference with no real consequences. I was wrong. If you&amp;rsquo;re running a Linux media server and care about performance, compatibility, and sane file permissions, the protocol you choose will define your entire experience. Pick wrong, and you&amp;rsquo;ll spend weekends debugging permission errors instead of watching movies or shows.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the practical decision framework. Where each protocol wins, how to set them up on a Linux server, which settings move the needle, and the gotchas that bite people every weekend on r/homelab. Mixed-OS house, or trying to reach your Unraid shares from a Windows laptop? Read on.&lt;/p&gt;
&lt;h2 id="nfs-vs-smb-what-each-protocol-does"&gt;NFS vs SMB: What Each Protocol Does
&lt;/h2&gt;&lt;p&gt;Network File System (NFS) is the native file-sharing protocol for Unix and Linux systems. It makes remote folders appear as local directories, with tight integration into the Linux kernel. It&amp;rsquo;s how Linux prefers to share files across a network.&lt;/p&gt;
&lt;p&gt;Server Message Block (SMB) is Windows&amp;rsquo; native file-sharing protocol. Linux systems access SMB shares through Samba, while macOS speaks SMB natively. SMB packs more features and integrates deeply with Windows authentication and permissions.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;NFS&lt;/th&gt;
&lt;th&gt;SMB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Linux-to-Linux&lt;/td&gt;
&lt;td&gt;Mixed OS / Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default on&lt;/td&gt;
&lt;td&gt;Linux/Unix&lt;/td&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;NFSv4 + Kerberos&lt;/td&gt;
&lt;td&gt;SMB3 built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance (small files, Linux)&lt;/td&gt;
&lt;td&gt;Faster&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance (large files)&lt;/td&gt;
&lt;td&gt;Similar&lt;/td&gt;
&lt;td&gt;Similar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows support&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker/container use&lt;/td&gt;
&lt;td&gt;Common&lt;/td&gt;
&lt;td&gt;Less common&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Simple (Linux)&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Why this matters for your media server: it constantly reads metadata, generates thumbnails, and streams video files. The protocol you pick decides how fast library scans finish, how snappy your media player UI feels, and whether file permissions stay sane or slowly drive you insane.&lt;/p&gt;
&lt;p&gt;Your clients&amp;rsquo; operating systems drive the decision. Running a Linux media server accessed only by other Linux machines? NFS typically performs better. Mixed environments with Windows, macOS, and Linux clients? SMB is the sane choice.&lt;/p&gt;
&lt;p&gt;Both protocols can be properly secured on a trusted LAN, but their default configurations and hardening approaches vary significantly. Poor defaults have real consequences. SMBv1 vulnerabilities enabled major malware outbreaks. Misconfigured NFS exports can expose your data to anyone on the network. Neither protocol forgives carelessness.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0G1C1YNN8"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ugreen-NAS_hu_9b2b8c726e38b8a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ugreen-NAS_hu_7dd7553917d2893c.webp" width="600" height="353" alt="UGREEN NASync DXP4800 Pro 4-Bay Desktop NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;UGREEN NASync DXP4800 Pro 4-Bay Desktop NAS&lt;/strong&gt;&lt;br&gt;
Four bays, an Intel Core i3-1315U, and dual networking (10GbE + 2.5GbE). Supports both NFS and SMB out of the box, so you can test what you&amp;rsquo;re reading about here instead of theorizing. A solid entry point if you don&amp;rsquo;t already have a NAS to experiment with.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4cmMHVT" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="nfs-or-smb-which-protocol-should-you-use"&gt;NFS or SMB: Which Protocol Should You Use?
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;All clients are Linux, Unix, or Android TV?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick NFS. It&amp;rsquo;s fast, mature, and simple for Linux-to-Linux communication. You&amp;rsquo;ll avoid the worst of the permission headaches, provided you manage your UIDs and GIDs correctly. (More on that later.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Windows or macOS clients in the mix?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick SMB. It&amp;rsquo;s native on Windows, works smoothly on macOS, and Samba on Linux is well-supported and battle-tested. If specific Linux-only apps benefit from NFS, you can add it alongside SMB, but let SMB be the default.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Security and centralized authentication matter?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SMB3 with modern authentication and encryption is the more straightforward path, especially in Windows and Samba environments. NFSv4 with Kerberos can be equally secure, but the setup complexity is significantly higher. If you&amp;rsquo;re not doing Kerberos, restrict NFS to trusted subnets and use strong firewall rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Running Unraid or another NAS?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unraid and most NAS platforms support both protocols. Use SMB for Windows clients, NFS for Linux clients or Docker containers. Don&amp;rsquo;t expose either protocol to the internet. Ever.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Both protocols are fast enough for streaming large media files. The performance gap shows up when your media manager scans thousands of tiny images and NFO files. Linux clients usually chew through those faster over NFS, while Windows clients do better with SMB thanks to client stack optimizations.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="nfs-vs-smb-pros-and-cons"&gt;NFS vs SMB Pros and Cons
&lt;/h2&gt;&lt;h3 id="nfs-strengths-and-weaknesses"&gt;NFS: Strengths and Weaknesses
&lt;/h3&gt;&lt;h4 id="pros"&gt;Pros:
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Fast on Linux.&lt;/strong&gt; Lower protocol overhead and native kernel modules give NFS an edge when dealing with many small file operations in Linux-only environments. For a Linux media server streaming content to other Linux clients and managing large collections, this matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simple integration.&lt;/strong&gt; NFS is part of the Linux ecosystem. Mounts, fstab, systemd units, and permissions all make sense if you track your UIDs and GIDs. No wrestling with foreign concepts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stateless heritage.&lt;/strong&gt; Classic NFSv3 is largely stateless, which can simplify some failure recoveries.&lt;/p&gt;
&lt;h4 id="cons"&gt;Cons:
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Security by default is weaker.&lt;/strong&gt; Older NFS versions lack encryption and rely on client-side UID/GID trust. NFSv4 improves this and supports Kerberos, but few home labs set that up correctly. Most people live with the risk and lock things down with firewall rules. Know that you&amp;rsquo;re making that trade-off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows support is painful.&lt;/strong&gt; Windows can mount NFS, but the built-in client is inconsistent across editions and awkward to configure. If you need to access shares from Windows regularly, save yourself the grief and use SMB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Focused feature set.&lt;/strong&gt; NFS does file and directory sharing well. That&amp;rsquo;s it. Don&amp;rsquo;t come looking for the broader Windows ecosystem features that SMB provides.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best fit:&lt;/strong&gt;
Linux media servers and clients, Docker workloads, Kubernetes, and Linux-only NAS access.&lt;/p&gt;
&lt;h3 id="smb-strengths-and-weaknesses"&gt;SMB: Strengths and Weaknesses
&lt;/h3&gt;&lt;h4 id="pros-1"&gt;Pros:
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Cross-platform.&lt;/strong&gt; Native on Windows, solid on macOS and Linux via Samba. For mixed-OS homes and offices, SMB works without drama. That&amp;rsquo;s not praise you hand out lightly in this industry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Modern security.&lt;/strong&gt; SMB3 supports strong authentication and optional encryption, integrates with Active Directory, and includes features like signing and multichannel. When configured properly, it&amp;rsquo;s solid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature-rich.&lt;/strong&gt; Beyond files, SMB supports printers and deep Access Control List (ACL) capabilities that align with Windows permission models. If you have a Windows background, the permissions will feel familiar.&lt;/p&gt;
&lt;h4 id="cons-1"&gt;Cons:
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;More overhead.&lt;/strong&gt; Often slower than NFS in Linux-to-Linux scenarios with many small files, though SMB3 performance can be excellent with Windows clients. The extra features come at a cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;More knobs to misconfigure.&lt;/strong&gt; Samba has a lot of options, and it&amp;rsquo;s easy to botch permissions or lose performance with certain security defaults. The flexibility is both a blessing and a trap.&lt;/p&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
SMBv1 is insecure and was exploited in major malware outbreaks like WannaCry. Disable it everywhere. No exceptions, no excuses, no &amp;ldquo;but my old printer needs it.&amp;rdquo; If a device only speaks SMBv1, that device has a retirement date, and it&amp;rsquo;s today.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Best fit:&lt;/strong&gt;
Mixed OS environments, Windows clients, or macOS systems.&lt;/p&gt;
&lt;h2 id="preparing-your-linux-server-or-nas"&gt;Preparing Your Linux Server or NAS
&lt;/h2&gt;&lt;p&gt;Whether you&amp;rsquo;re running a bare Linux server, Unraid, Synology, TrueNAS, or another NAS platform, the setup approach stays consistent. Sort this out before you touch a config file:&lt;/p&gt;
&lt;p&gt;Figure out which shares will be accessed by which clients. Use SMB for Windows and macOS connections. Use NFS for Linux apps and clients. Avoid exporting the same path through both protocols unless you understand the consequences for permissions and extended attributes (and you probably don&amp;rsquo;t yet, which is fine). Keep both protocols scoped to your trusted LAN. Never port-forward 445 or 2049 from your router to the internet.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
That last point is worth repeating: exposing file share ports to the internet is begging for trouble. Your media server doesn&amp;rsquo;t need to be the next cautionary tale in network security forums. People will screenshot your misconfiguration and post it for laughs.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;On Unraid specifically:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SMB comes enabled by default, making Windows access straightforward. You can enable NFS per share for Linux clients and containers. Mapping Windows drives to SMB shares is point-and-click simple. Accessing NFS shares from Windows, on the other hand, requires client-side tools and sometimes Windows Enterprise features. For most home setups, SMB handles Windows clients fine. Don&amp;rsquo;t add an unnecessary headache.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On Synology DSM:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Synology supports both NFS and SMB through its Control Panel. SMB is enabled by default. To enable NFS, go to Control Panel &amp;gt; File Services &amp;gt; NFS and check &amp;ldquo;Enable NFS service.&amp;rdquo; You can then set NFS permissions per shared folder under the folder&amp;rsquo;s edit settings. Synology makes this relatively painless, but pay attention to the squash settings and allowed IP ranges. The defaults are conservative, which is the right instinct, but they&amp;rsquo;ll block access if you don&amp;rsquo;t explicitly allow your client subnets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On TrueNAS (SCALE and CORE):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TrueNAS supports both protocols through its web UI. Create a dataset first, then configure SMB or NFS sharing on top of it. For Jellyfin or Plex running in a TrueNAS container or jail, NFS is the common choice for mounting media storage. Be aware that TrueNAS SCALE uses Linux under the hood while TrueNAS CORE uses FreeBSD, which can affect how NFS permissions behave. If you&amp;rsquo;re running apps on TrueNAS SCALE, pointing them at an NFS share from a separate dataset avoids the host path permission headaches that catch a lot of people.&lt;/p&gt;
&lt;h2 id="setting-up-smb-on-a-linux-server-samba"&gt;Setting Up SMB on a Linux Server (Samba)
&lt;/h2&gt;&lt;p&gt;SMB on Linux is straightforward once you&amp;rsquo;ve seen it done. Here&amp;rsquo;s how to set up a media share that Windows, macOS, and other Linux machines can all reach without drama.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/smb-guide/featured_hu_5cabe0d87cd73a19.avif"&gt;&lt;img src="https://diymediaserver.com/post/smb-guide/featured_hu_e2c37b81f34d44b2.webp" width="100" height="100" alt="Follow my SMB installation and Configuration Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Follow my SMB installation and Configuration Guide&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/smb-guide/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Security Notes:&lt;/strong&gt;
Use SMBv3 or higher wherever possible. Disable SMBv1 entirely. If you&amp;rsquo;ve read this far, and it&amp;rsquo;s still enabled on your network, go fix that now. I&amp;rsquo;ll wait.&lt;/p&gt;
&lt;p&gt;Follow the linked guide and you get an authenticated share that holds up across Windows, macOS, and Linux. Your media server stops fighting Windows clients over permissions, which is half the reason people give up on Samba in the first place.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
Dual 2.5GbE with a 10GbE option, a Ryzen CPU, and triple NVMe slots in a box smaller than most routers. If you want a Proxmox host that can run NFS and SMB servers in VMs or containers without breaking a sweat, this is the kind of hardware that makes it trivial.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="setting-up-nfs-on-a-linux-server"&gt;Setting Up NFS on a Linux Server
&lt;/h2&gt;&lt;p&gt;NFS on a Linux media server is the easier of the two to stand up. Here&amp;rsquo;s the configured-and-mounted version:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/nfs-guide/featured_hu_98dbc6091e230973.avif"&gt;&lt;img src="https://diymediaserver.com/post/nfs-guide/featured_hu_8055e6719125f890.webp" width="100" height="100" alt="Follow my NFS installation and Configuration Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Follow my NFS installation and Configuration Guide&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/nfs-guide/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;For those who want the quick version, here&amp;rsquo;s what mounting an NFS share from a Linux client looks like in practice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# One-time manual mount to test the connection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -t nfs4 192.168.1.100:/mnt/media /mnt/nfs/media
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Persistent mount via /etc/fstab (survives reboots)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Add this line:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.1.100:/mnt/media /mnt/nfs/media nfs4 rw,noatime,rsize&lt;span class="o"&gt;=&lt;/span&gt;1048576,wsize&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1048576&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the IP and paths with your own. Test the manual mount first. If that works, add the &lt;code&gt;fstab&lt;/code&gt; entry. If the manual mount fails, don&amp;rsquo;t bother with &lt;code&gt;fstab&lt;/code&gt; until you&amp;rsquo;ve sorted out the underlying issue. Debugging a boot hang caused by a bad NFS mount in &lt;code&gt;fstab&lt;/code&gt; is a miserable experience. Ask me how I know. If you&amp;rsquo;re worried about boot hangs, add the &lt;code&gt;nofail&lt;/code&gt; option, so the system boots even if the NFS server is unreachable.&lt;/p&gt;
&lt;h2 id="nfs-and-smb-security-best-practices"&gt;NFS and SMB Security Best Practices
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing about network file sharing: convenience and security pull in opposite directions. But a few smart choices upfront will save you from becoming internet famous for all the wrong reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep it local.&lt;/strong&gt; SMB and NFS belong on your trusted LAN only. Never expose ports 445 (SMB) or 2049 (NFS) to the internet. Block them at your router and firewall. This isn&amp;rsquo;t paranoia. This is basic hygiene.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMB security essentials:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Disable SMBv1 entirely. It&amp;rsquo;s ancient, vulnerable, and has no place in modern networks. Use SMBv2 or SMBv3. Enable strong authentication and SMB encryption where your data sensitivity justifies the CPU overhead. Yes, encryption costs performance, but that&amp;rsquo;s a trade-off worth making for sensitive content. If you need centralized user management, integrate with existing directory services rather than managing local accounts one by one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS security essentials:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Stick with NFSv4 over older versions. Restrict exports by IP or subnet. Use &lt;code&gt;root_squash&lt;/code&gt; to prevent compromised clients from running wild with root privileges. Deploy host-based firewalls to allow only known clients. Defense in depth matters because single points of failure are single points of regret.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The backup reality check:&lt;/strong&gt;
Neither SMB nor NFS will save you from accidental deletion or ransomware. Keep offline or versioned backups of your important data. This is not optional. Your future self will thank you, or your current self will learn the hard way.&lt;/p&gt;
&lt;h2 id="nfs-and-smb-performance-tuning"&gt;NFS and SMB Performance Tuning
&lt;/h2&gt;&lt;p&gt;Tuning network shares isn&amp;rsquo;t deep magic. A handful of small changes will move you from &amp;ldquo;fine&amp;rdquo; to &amp;ldquo;noticeably faster.&amp;rdquo; Here&amp;rsquo;s what pays off:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For large sequential reads (streaming movies), the default settings are usually fine. For many small files, look at &lt;code&gt;actimeo&lt;/code&gt; (attribute cache timing) and consider &lt;code&gt;noatime&lt;/code&gt; to reduce metadata writes. Use NFSv4.2 when available. It cuts down on port sprawl and handles flaky network connections better. You can bump up &lt;code&gt;rsize/wsize&lt;/code&gt; (try &lt;code&gt;rsize=1048576,wsize=1048576&lt;/code&gt; as a starting point), but test with your specific NIC and switch setup. Bigger buffers aren&amp;rsquo;t always better buffers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SMB:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Enable SMB multichannel on capable clients and servers. It bonds multiple NICs or network paths for better throughput and fault tolerance. Stick with SMB version 3.x or higher.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Both protocols:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Jumbo frames (MTU 9000) can help, but only if every single network hop supports the same MTU. Miss one switch or router, and you&amp;rsquo;ll actually make things worse. Always test with tools like &lt;code&gt;robocopy&lt;/code&gt; (Windows), &lt;code&gt;fio&lt;/code&gt;, or &lt;code&gt;dd&lt;/code&gt; to measure before and after. A simple sequential read test with &lt;code&gt;fio&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fio --name&lt;span class="o"&gt;=&lt;/span&gt;seqread --rw&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; --bs&lt;span class="o"&gt;=&lt;/span&gt;1M --size&lt;span class="o"&gt;=&lt;/span&gt;1G --numjobs&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; --filename&lt;span class="o"&gt;=&lt;/span&gt;/mnt/yourshare/testfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Change one variable at a time. Otherwise you&amp;rsquo;ll never know what helped and what hurt.&lt;/p&gt;
&lt;h3 id="nfs-vs-smb-speed-what-the-benchmarks-show"&gt;NFS vs SMB Speed: What the Benchmarks Show
&lt;/h3&gt;&lt;p&gt;People love to argue about NFS vs SMB performance in the abstract. Here&amp;rsquo;s what the numbers look like in real homelab conditions.&lt;/p&gt;
&lt;p&gt;For sequential reads and writes (streaming a movie, copying a large file), NFS and SMB perform within a few percent of each other on a modern gigabit or 2.5GbE link. Both protocols will saturate a 1GbE connection without breaking a sweat. You&amp;rsquo;re not going to notice a difference when playing a 30GB Blu-ray remux over either protocol.&lt;/p&gt;
&lt;p&gt;The gap opens up with small files. When your media manager scans a library of thousands of NFO files, poster images, and subtitle files, NFS on Linux clients can be 25-30% faster than SMB for random read operations. This is where NFS&amp;rsquo;s lower protocol overhead and kernel-level integration actually matter. If your Jellyfin library scan takes 20 minutes over SMB, it might take 14 over NFS. That adds up when you&amp;rsquo;re managing a large collection.&lt;/p&gt;
&lt;p&gt;SMB claws back some ground on random writes, where it&amp;rsquo;s sometimes faster depending on the server and client configuration. And on Windows clients, SMB&amp;rsquo;s native stack optimizations mean it frequently outperforms NFS, which is running through a clunkier client implementation on that platform.&lt;/p&gt;
&lt;p&gt;The takeaway: don&amp;rsquo;t pick a protocol based on speed alone. Pick based on your client OS, then tune for speed within that choice.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;br&gt;
Protocol benchmarks don&amp;rsquo;t mean much against a handful of test files. You need a real media library to see the difference. 24TB gives you enough room to fill a NAS and stress-test your NFS or SMB configuration with thousands of files, not a synthetic toy workload.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="common-nfs-and-smb-pitfalls"&gt;Common NFS and SMB Pitfalls
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Mixing NFS and SMB on the same share:&lt;/strong&gt;&lt;br&gt;
This is a deep topic, but the short version: Windows ACLs and POSIX permissions are like oil and water. Samba stores NT ACLs as extended attributes, but NFS clients may not preserve them. If you must mix protocols on the same data, separate your write paths or designate one protocol for writes and keep the other read-only.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UID/GID mismatches on NFS:&lt;/strong&gt;&lt;br&gt;
Keep your user and group IDs consistent across all Linux clients. Use centralized identity management (LDAP/SSSD) or carefully align your &lt;code&gt;/etc/passwd&lt;/code&gt; entries. For a quick check, run &lt;code&gt;id username&lt;/code&gt; on both client and server and make sure the numbers match. Debugging permission issues caused by mismatched IDs is not how you want to spend your weekend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Assuming &amp;ldquo;NFS is insecure&amp;rdquo; or &amp;ldquo;SMB is slow&amp;rdquo;:&lt;/strong&gt;&lt;br&gt;
These are lazy generalizations. NFSv4 with Kerberos holds up against serious threat models. SMB3 on modern stacks is both fast and secure. The real performance and security outcomes depend on your configuration and client mix, not the protocol name on the tin.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Turning on async writes blindly on NFS:&lt;/strong&gt;&lt;br&gt;
Async writes (&lt;code&gt;async&lt;/code&gt; in &lt;code&gt;/etc/exports&lt;/code&gt;) can boost speed, but at the cost of data safety during crashes. If the server goes down mid-write, you can lose data. Understand the trade-off before flipping that switch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exposing shares directly to the internet:&lt;/strong&gt;&lt;br&gt;
This is how you get pwned. Use VPNs for remote access. Never use direct port forwards for file shares. Not &amp;ldquo;probably don&amp;rsquo;t.&amp;rdquo; Don&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="example-scenarios-what-to-choose"&gt;Example Scenarios: What To Choose
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s how to pick the right protocol based on your actual setup:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All Linux home:&lt;/strong&gt;&lt;br&gt;
Export media via NFSv4, mount on Jellyfin/Plex and your Linux desktop. Keep UIDs aligned across systems. Optionally offer SMB read-only for occasional Windows use. This gives you the performance benefits of NFS where it matters most while keeping a door open for Windows guests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows family PCs, macOS laptop, Linux server NAS:&lt;/strong&gt;&lt;br&gt;
Use SMB for everyone. Enable NFS only for Linux containers or hosts that specifically benefit from it. Keep permissions in one place through Samba. Fighting permission mismatches across protocols isn&amp;rsquo;t worth the headache when SMB works fine for the whole house.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unraid backing multiple Docker apps and a Windows gaming PC:&lt;/strong&gt;&lt;br&gt;
Give containers NFS or bind mounts for speed. Export the same content over SMB to Windows, but consider separate paths or read-only mounts to avoid permission drift. This hybrid approach lets your containers stay fast while keeping Windows happy. Watch for permission conflicts if both protocols write to the same files. When in doubt, pick one writer.&lt;/p&gt;
&lt;h3 id="nfs-vs-smb-for-docker-containers"&gt;NFS vs SMB for Docker Containers
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;re running Plex, Jellyfin, Sonarr, Radarr, or any other media app in Docker, you have three main options for accessing remote storage: NFS mounts, SMB/CIFS mounts, or bind mounts from a path already mounted on the host.&lt;/p&gt;
&lt;p&gt;The cleanest approach for most homelabs is to mount the NFS share on the Docker host, then bind-mount that path into your containers. This keeps the NFS configuration in one place (the host&amp;rsquo;s &lt;code&gt;/etc/fstab&lt;/code&gt;) and lets every container access the same media library without each one needing its own network mount logic. Here&amp;rsquo;s what that looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On the Docker host, mount the NFS share&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.1.100:/mnt/media /mnt/nfs/media nfs4 rw,noatime,nofail &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# In docker-compose.yml, bind-mount it into the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - /mnt/nfs/media:/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; mount SMB/CIFS shares into Docker containers using a CIFS volume driver or by mounting on the host first, but NFS is generally the better fit here. It avoids the SMB authentication layer, has less overhead for the constant small-file reads that media apps love to do, and plays nicer with Linux permissions inside containers. If your NAS is a Synology, TrueNAS, or Unraid box, enable NFS for these container workloads even if the rest of your house uses SMB.&lt;/p&gt;
&lt;p&gt;One warning: make sure the NFS share is mounted before Docker starts your containers. If the mount isn&amp;rsquo;t ready and your container writes to what it thinks is the media directory, those writes land on the host&amp;rsquo;s local filesystem instead. You&amp;rsquo;ll spend an hour wondering why your files disappeared. Use &lt;code&gt;nofail&lt;/code&gt; and &lt;code&gt;x-systemd.automount&lt;/code&gt; in your fstab, or set up a systemd dependency so Docker waits for the mount.&lt;/p&gt;
&lt;h3 id="nfs-vs-smb-for-proxmox"&gt;NFS vs SMB for Proxmox
&lt;/h3&gt;&lt;p&gt;Proxmox users face this question constantly, especially when adding NAS storage as a datastore for VMs, containers, or ISO images. Proxmox supports both NFS and SMB/CIFS as storage backends, configurable directly from the Datacenter &amp;gt; Storage menu in the web UI.&lt;/p&gt;
&lt;p&gt;For most Proxmox homelabs, NFS is the better default. Proxmox runs Linux under the hood, NFS is a first-class citizen, and the integration is straightforward. Add your NAS as an NFS datastore, point it at the export path, and Proxmox handles the rest. VM disk images, container templates, backups, and ISOs can all live on NFS storage without issue.&lt;/p&gt;
&lt;p&gt;SMB/CIFS storage is also supported in Proxmox, but it&amp;rsquo;s more commonly used when the storage backend is a Windows server or when NFS isn&amp;rsquo;t available. If your NAS supports both, pick NFS for Proxmox and save SMB for your Windows and macOS clients.&lt;/p&gt;
&lt;p&gt;One Proxmox-specific gotcha: if you&amp;rsquo;re passing NFS-mounted storage into an LXC container (not a VM), pay attention to UID/GID mapping. Unprivileged containers remap UIDs by default, which means UID 1000 inside the container becomes UID 101000 on the host and on the NFS share. Either use privileged containers for media workloads, manually map the UIDs in the container config, or accept that permissions will fight you until you sort it out.&lt;/p&gt;
&lt;h2 id="troubleshooting-nfs-and-smb-connections"&gt;Troubleshooting NFS and SMB Connections
&lt;/h2&gt;&lt;p&gt;When things go sideways (and they will), here&amp;rsquo;s how to get back on track.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connection refused or cannot mount:&lt;/strong&gt;&lt;br&gt;
Check if your services are actually running: &lt;code&gt;systemctl status smbd nmbd&lt;/code&gt; for SMB, or &lt;code&gt;systemctl status nfs-server&lt;/code&gt; for NFS. Verify firewall rules. SMB needs TCP 445, NFSv4 needs TCP 2049. If you&amp;rsquo;re stuck with NFSv3, &lt;code&gt;rpcbind&lt;/code&gt; and &lt;code&gt;mountd&lt;/code&gt; may need additional ports opened. Double-check your server IP and share names. Yes, typos happen to all of us.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Permission denied:&lt;/strong&gt;&lt;br&gt;
For SMB, verify the user exists in both Linux and Samba (&lt;code&gt;pdbedit -L&lt;/code&gt; lists Samba users), check &lt;code&gt;smbpasswd&lt;/code&gt;, and validate share-level access controls. For NFS, UID/GID mismatches are the usual suspect. Align user IDs between systems or configure &lt;code&gt;idmapd&lt;/code&gt; for NFSv4. Also make sure your export options aren&amp;rsquo;t accidentally set to &lt;code&gt;ro&lt;/code&gt; (read-only) when you meant &lt;code&gt;rw&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slow file transfers:&lt;/strong&gt;&lt;br&gt;
Check what protocol version you&amp;rsquo;re actually using. Force &lt;code&gt;vers=3.0&lt;/code&gt; for SMB or &lt;code&gt;vers=4.2&lt;/code&gt; for NFS and test again. Disable Wi-Fi power saving on clients and test over wired connections. For SMB, experiment with multichannel, signing, or encryption settings to understand the performance trade-offs. Don&amp;rsquo;t disable security features on untrusted networks. For NFS, try different &lt;code&gt;rsize/wsize&lt;/code&gt; values and check for duplicate mounts or DNS slowness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows cannot access NFS:&lt;/strong&gt;&lt;br&gt;
The Client for NFS feature might be missing or unsupported in your Windows edition (Home edition doesn&amp;rsquo;t have it). Stick with SMB for Windows clients and save NFS for Linux systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Media server doesn&amp;rsquo;t see new files immediately:&lt;/strong&gt;&lt;br&gt;
For SMB, Samba uses change notifications, but some applications still cache directory listings. Try a manual library refresh or verify your app is watching the correct path. For NFS, attribute caching can delay file visibility. Adjust &lt;code&gt;actimeo=0&lt;/code&gt; for immediate visibility, but expect a performance hit. Find the balance that works for your scan frequency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Library scans crawl along:&lt;/strong&gt;&lt;br&gt;
For many small files, NFS on Linux clients often outperforms SMB. Consider running your media scanner on a Linux machine over NFS, even if end users stream via SMB.&lt;/p&gt;
&lt;h2 id="faqs"&gt;FAQs:
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What is the difference between NFS and SMB for a home media server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NFS is Linux&amp;rsquo;s native file sharing protocol and excels at Linux-to-Linux performance with minimal overhead. SMB is Windows&amp;rsquo; native protocol and works best in mixed OS environments, offering strong security features and broad client support. If your media server runs Linux but you have Windows clients, SMB is usually the safer bet.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does Unraid support NFS and SMB shares?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, Unraid supports both protocols out of the box. SMB is enabled by default for Windows compatibility, while NFS is commonly used for Linux clients and Docker containers. You can enable either or both, depending on your needs.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I access an NAS or Unraid share from Windows?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use SMB. In Windows Explorer, type &lt;code&gt;\\unraid-hostname\sharename&lt;/code&gt; in the address bar or map it as a network drive. While Windows 10 Pro and Enterprise can mount NFS shares, SMB is the path of least resistance for most home users.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which is faster for Plex or Jellyfin: NFS or SMB?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;For Linux media servers scanning thousands of small files (like movie thumbnails), NFS typically wins due to lower protocol overhead. Windows clients often see better performance with SMB3. When streaming large video files, both protocols are plenty fast. Compatibility matters more than raw speed at that point.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is NFS secure enough for my home network?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NFS can be secure on a trusted home network when properly configured with export restrictions and firewall rules. For enhanced security, consider upgrading to NFSv4 with Kerberos authentication, though this significantly increases setup complexity. Most importantly, never expose NFS directly to the internet. Always use a VPN for remote access.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I set up NFS shares on Unraid?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Enable NFS in Unraid&amp;rsquo;s settings, configure per-share export rules, and specify allowed client IPs or subnets. Mount from Linux clients using &lt;code&gt;mount -t nfs server:/export /mnt&lt;/code&gt;. The key is getting the export permissions right the first time.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can I use both NFS and SMB on the same Unraid share?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Technically yes. But you&amp;rsquo;re asking for trouble. Permission conflicts and extended attribute mismatches can cause real headaches. If you must use both, consider making one protocol read-only or carefully separating write operations between protocols.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why am I getting permission denied when mounting an NFS share?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Usually, it&amp;rsquo;s UID/GID mismatches between your client and server, or restrictive export rules. Ensure user IDs align across your Linux systems and double-check &lt;code&gt;/etc/exports&lt;/code&gt; for correct options and allowed subnets. This is the most common NFS stumbling block.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I enable SMB3 encryption on my server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;In your Samba configuration, set &lt;code&gt;smb encrypt = required&lt;/code&gt; or &lt;code&gt;desired&lt;/code&gt; globally or per-share. Keep in mind that encryption adds CPU overhead. Test performance impacts on your hardware before committing.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What are the risks of exposing SMB or NFS to the internet?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;High risk. Don&amp;rsquo;t do it. SMBv1 was exploited in major ransomware outbreaks like WannaCry. Even modern SMB and NFS implementations are frequent attack targets. Use a VPN for remote access and never port-forward 445 or 2049 from your router.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I fix slow file transfers over SMB or NFS?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Start with the basics: verify you&amp;rsquo;re using modern protocol versions, test with wired connections, and benchmark one change at a time. For NFS, tune &lt;code&gt;rsize&lt;/code&gt; and &lt;code&gt;wsize&lt;/code&gt; parameters. For SMB, enable multichannel if your hardware supports it.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use NFSv3 or NFSv4 for my media server?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Go with NFSv4 unless you have a specific reason not to. It simplifies port management, provides better file locking, and supports Kerberos security. Only stick with NFSv3 if you&amp;rsquo;re dealing with legacy systems that genuinely can&amp;rsquo;t handle v4.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I map Linux users to Windows users for file access?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;With SMB, configure Samba&amp;rsquo;s &lt;code&gt;idmap&lt;/code&gt; backends and maintain consistent POSIX ACLs for cross-platform access. The Samba docs on idmap are dense but worth reading. Getting this right once saves you from ongoing permission fights.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use NFS or SMB for Docker containers?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NFS is generally the better choice for Docker containers on Linux. Mount the NFS share on the Docker host, then bind-mount the path into your containers. This avoids SMB&amp;rsquo;s authentication overhead and plays nicer with Linux permissions inside containers. Make sure the NFS mount is ready before Docker starts your containers, or you&amp;rsquo;ll write to the local filesystem by mistake.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use NFS or SMB with Proxmox?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NFS is the more natural fit for Proxmox since it runs Linux. Add your NAS as an NFS datastore through the Proxmox web UI under Datacenter &amp;gt; Storage. It works well for VM disk images, container templates, backups, and ISOs. Use SMB/CIFS in Proxmox only if your storage backend is a Windows server or NFS isn&amp;rsquo;t available.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does Synology support NFS and SMB?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. Synology DSM supports both protocols. SMB is enabled by default. Enable NFS through Control Panel &amp;gt; File Services &amp;gt; NFS, then set NFS permissions per shared folder. Most Synology users run SMB for desktop clients and NFS for Linux servers, Docker hosts, or Proxmox datastores.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ NFS or SMB for TrueNAS media storage?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;TrueNAS supports both. For media apps like Plex or Jellyfin running in TrueNAS containers or jails, NFS is the common choice. Create a dataset, configure NFS sharing on it, and point your media app at the NFS mount. Be aware that TrueNAS SCALE (Linux-based) and TrueNAS CORE (FreeBSD-based) handle NFS permissions slightly differently.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-make-the-choice-that-fits-your-clients-and-your-risk"&gt;Conclusion: Make the Choice That Fits Your Clients and Your Risk
&lt;/h2&gt;&lt;p&gt;The decision comes down to matching your protocol to your environment and accepting the trade-offs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If your world is Linux,&lt;/strong&gt; NFS is the pragmatic, fast, and simple default. It speaks the same language as your filesystem and won&amp;rsquo;t fight you on permissions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you have Windows or macOS clients,&lt;/strong&gt; SMB is the native choice that works. SMB3 brings solid security without requiring a PhD in Kerberos configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can run both protocols,&lt;/strong&gt; but don&amp;rsquo;t blindly export the same writeable path over both unless you understand how permissions and metadata will clash. That way lies madness and mysterious &amp;ldquo;permission denied&amp;rdquo; errors you will never reproduce on demand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lock it down.&lt;/strong&gt; Keep shares on your LAN, disable SMBv1 (seriously, it&amp;rsquo;s 2025), prefer NFSv4, and consider Kerberos or SMB encryption where your threat model demands it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next steps:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pick one protocol per client type and start simple. Don&amp;rsquo;t overcomplicate your first setup. Benchmark your scans and streams to establish a baseline. Layer on security and performance tuning only after you have a stable foundation.&lt;/p&gt;
&lt;p&gt;Linux media server, Windows laptop hitting an Unraid share, doesn&amp;rsquo;t really matter. The choice matters less than knowing why you chose. Stop treating network protocols like a coin flip. Match them to your clients, and the whole stack gets faster, safer, and quieter.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;&lt;br&gt;
Hard to compare NFS and SMB performance when your NIC is the bottleneck. This 2.5GbE PCIe card is cheap and works out of the box on most Linux distros and Windows. It gets your gigabit ceiling out of the way so you can see what the protocols are doing.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How to Install Docker on Debian for GPU Passthrough and Transcoding</title><link>https://diymediaserver.com/post/install-docker-debian-gpu-passthrough-transcoding/</link><pubDate>Wed, 27 Aug 2025 10:03:58 -0600</pubDate><guid>https://diymediaserver.com/post/install-docker-debian-gpu-passthrough-transcoding/</guid><description>&lt;img src="https://diymediaserver.com/post/install-docker-debian-gpu-passthrough-transcoding/featured_hu_f92a4853d5e7cce.webp" alt="Featured image of post How to Install Docker on Debian for GPU Passthrough and Transcoding" /&gt;&lt;p&gt;You want buttery-smooth 4K to 1080p hardware transcoding, and the quiet satisfaction of watching your CPU lounge around at 10% while your GPU does the heavy lifting. So you pass your GPU into a Docker container, and Jellyfin stares back at you with the enthusiasm of a DMV clerk. CPU&amp;rsquo;s pegged. Jellyfin&amp;rsquo;s logs throwing a tantrum about &amp;ldquo;no such device /dev/dri/renderD128.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Welcome to the club. Population: everyone who has ever tried this.&lt;/p&gt;
&lt;p&gt;I once burned a perfectly good Friday night (116 minutes, to be precise) wrestling with what Docker&amp;rsquo;s documentation cheerfully calls &amp;ldquo;simple&amp;rdquo; GPU passthrough. Drivers? Rock solid. Docker daemon? Restarted three times, because why stop at two. YAML file? Formatted with the precision of a Swiss watchmaker. The actual problem? Linux permissions had decided my container wasn&amp;rsquo;t &amp;lsquo;special&amp;rsquo; enough for the render group&amp;rsquo;s exclusive party.&lt;/p&gt;
&lt;p&gt;After a scenic tour through udev rules, cgroup mysteries, and two spectacularly wrong Reddit threads that shall remain nameless, the GPU finally woke up. Frame times dropped from &amp;ldquo;slideshow&amp;rdquo; to &amp;ldquo;silk.&amp;rdquo; My CPU went back to napping. And I made myself a promise to document this mess before my brain forgot the crucial details.&lt;/p&gt;
&lt;p&gt;This is that documentation. The step-by-step guide I needed that night. You&amp;rsquo;ll get the exact Docker Compose configuration, the permissions that matter, and a heads-up about the gotchas that love to bite newcomers. Whether you&amp;rsquo;re on NVIDIA, Intel integrated graphics, or AMD, by the end of this you&amp;rsquo;ll have hardware-accelerated transcoding humming inside Docker. No marathon debugging session required.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Think Docker will magically use your GPU? Cute. NVIDIA needs &amp;ndash;gpus, Intel/AMD need /dev/dri, and you need to join the right groups. Otherwise, enjoy watching your CPU sweat bullets.
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;
The Arc A580 can hold its own in games, but here it’s for obliterating video streams. With support for H.264, HEVC, and full AV1 hardware encode/decode across two media engines, it crushes 20+ 1080p streams or 6–8 HDR tone-mapped 4Ks without breaking a sweat. Drop it in your media server, give Jellyfin direct VA-API access, and watch your CPU finally cool off for a bit.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;That card up there is what I&amp;rsquo;d buy today if I was starting fresh. But the box won&amp;rsquo;t help you if Docker can&amp;rsquo;t see it. So let&amp;rsquo;s talk about why this whole setup falls over so often.&lt;/p&gt;
&lt;h2 id="why-gpu-passthrough-matters"&gt;Why GPU Passthrough Matters
&lt;/h2&gt;&lt;p&gt;Video transcoding hammers a CPU. Hardware acceleration through GPU features (NVIDIA NVENC, Intel VAAPI/QSV, AMD VCE) moves that work off the CPU, drops your load average, and quiets the fans.&lt;/p&gt;
&lt;p&gt;Docker lets you isolate Jellyfin or Plex in a container while still handing the GPU through in a controlled way. You get smoother streams, more simultaneous viewers, quieter hardware, and an idle CPU that finally earns its sleep state.&lt;/p&gt;
&lt;p&gt;One thing to keep straight: GPU passthrough in Docker is not the same as VM passthrough. Docker leans on device permissions and cgroups to share a GPU between host and container. It&amp;rsquo;s nowhere near as involved as full hardware virtualization through PCIe passthrough on a hypervisor.&lt;/p&gt;
&lt;h2 id="why-gpu-passthrough-in-docker-on-debian-often-fails-and-how-to-fix-it"&gt;Why GPU Passthrough in Docker on Debian Often Fails (And How to Fix It)
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing about GPU passthrough with Docker on Debian or any other Linux distro. It&amp;rsquo;s conceptually clean and practically finicky. Your GPU lives on the host, happily managed by kernel drivers. Your container exists in its own isolated environment with its own filesystem and permissions. Getting Docker and your GPU to talk takes more than mounting a device file. You need the right permissions, the correct device nodes, and sometimes a few udev rules to make everything line up.&lt;/p&gt;
&lt;p&gt;Think of it like lending your car to a friend. Handing over the keys (mounting the device) isn&amp;rsquo;t enough. They need insurance coverage (permissions), they need to know where it&amp;rsquo;s parked (device paths), and they need to understand the quirks (Linux-specific device behaviors).&lt;/p&gt;
&lt;p&gt;Common failures: your container sees the GPU but has no permission to use it, missing device nodes make the GPU invisible, or everything works until a reboot shuffles the device paths and breaks the mapping.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s fix all of that and get Docker GPU passthrough working reliably.&lt;/p&gt;
&lt;p&gt;Need a Linux permissions refresher?&lt;br&gt;
&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/" target="_blank" rel="noopener"
&gt;Understanding Linux Permissions&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="proper-docker-compose-configuration-for-gpu-passthrough-on-debian"&gt;Proper Docker Compose Configuration for GPU Passthrough on Debian
&lt;/h2&gt;&lt;p&gt;Forget juggling &lt;code&gt;docker run -it&lt;/code&gt; commands with a wall of flags. Here&amp;rsquo;s a straightforward Docker Compose setup for GPU passthrough that survives Debian reboots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jellyfin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin/jellyfin:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin-hw&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/dev/dri/renderD128:/dev/dri/renderD128 &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# GPU device access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;render&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Add container user to render group&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;video&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Add container user to video group&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;JELLYFIN_PublishedServerUrl=http://your-server-ip:8096&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./config:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./cache:/cache&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/path/to/media:/media:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8096:8096&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;devices: /dev/dri/renderD128:/dev/dri/renderD128&lt;/code&gt; hands the render node into the container, which is what VA-API needs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group_add&lt;/code&gt; puts the container user in the &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;video&lt;/code&gt; groups so the device permissions actually let it through.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt; brings Jellyfin back after a reboot or a crash, instead of leaving you to notice when someone tries to watch something.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="nvidia-vs-intelamd-gpu-passthrough-on-docker"&gt;NVIDIA vs Intel/AMD GPU Passthrough on Docker
&lt;/h2&gt;&lt;h3 id="nvidia-simplified-gpu-passthrough"&gt;NVIDIA: Simplified GPU Passthrough
&lt;/h3&gt;&lt;p&gt;Install the proprietary NVIDIA driver and the NVIDIA Container Toolkit on Debian. Then use &lt;code&gt;docker run -it --gpus all&lt;/code&gt; or the Compose &lt;code&gt;device_requests&lt;/code&gt; block. The toolkit handles the driver mapping and permissions for you.&lt;/p&gt;
&lt;p&gt;Good fit for transcoding, AI workloads, or anything else that wants CUDA.&lt;/p&gt;
&lt;h3 id="intelamd-manual-device-mapping"&gt;Intel/AMD: Manual Device Mapping
&lt;/h3&gt;&lt;p&gt;For Intel and AMD on Debian, you do the work yourself. Expose &lt;code&gt;/dev/dri&lt;/code&gt; to the container, manage the permissions, and make sure the right drivers are installed (&lt;code&gt;intel-media-va-driver&lt;/code&gt;, &lt;code&gt;mesa-va-drivers&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Add the container user to the &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;video&lt;/code&gt; groups or you&amp;rsquo;ll spend an hour debugging &lt;code&gt;Permission denied&lt;/code&gt; errors. Once it&amp;rsquo;s wired up, this approach is rock-solid for transcoding.&lt;/p&gt;
&lt;h2 id="preparing-your-debian-host-for-docker-gpu-passthrough"&gt;Preparing Your Debian Host for Docker GPU Passthrough
&lt;/h2&gt;&lt;p&gt;Before you touch Docker, prove the GPU works on the host. If &lt;code&gt;vainfo&lt;/code&gt; or &lt;code&gt;nvidia-smi&lt;/code&gt; doesn&amp;rsquo;t see it from the bare metal, no amount of YAML will help you.&lt;/p&gt;
&lt;h3 id="nvidia-setup-on-debian"&gt;NVIDIA Setup on Debian
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Install the NVIDIA proprietary drivers. Verify with &lt;code&gt;nvidia-smi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Install the NVIDIA Container Toolkit:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt-get install -y nvidia-container-toolkit
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nvidia-ctk runtime configure --runtime&lt;span class="o"&gt;=&lt;/span&gt;docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="3"&gt;
&lt;li&gt;Verify with:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm --gpus&lt;span class="o"&gt;=&lt;/span&gt;all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="intel-igpu-setup"&gt;Intel iGPU Setup
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Confirm the &lt;code&gt;i915&lt;/code&gt; kernel driver is loaded:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsmod &lt;span class="p"&gt;|&lt;/span&gt; grep i915
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;
&lt;li&gt;Check that &lt;code&gt;/dev/dri&lt;/code&gt; device nodes exist (&lt;code&gt;ls -l /dev/dri/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Install VA-API drivers:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install intel-media-va-driver vainfo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="4"&gt;
&lt;li&gt;Run &lt;code&gt;vainfo&lt;/code&gt; and look for a populated profile list. An empty list means the driver isn&amp;rsquo;t loading.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="docker-and-docker-compose-requirements-for-gpu-passthrough-on-debian"&gt;Docker and Docker Compose Requirements for GPU Passthrough on Debian
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Docker version 20.10 or newer (check with &lt;code&gt;docker --version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Docker Compose V2 (&lt;code&gt;docker compose&lt;/code&gt;, not the legacy &lt;code&gt;docker-compose&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A Linux kernel with cgroup v2 enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NVIDIA users need the runtime wired up through &lt;code&gt;nvidia-container-toolkit&lt;/code&gt;. Intel and AMD users only need correct device mapping and group membership.&lt;/p&gt;
&lt;h2 id="correct-gpu-passthrough-configuration-with-docker-compose-on-debian"&gt;Correct GPU Passthrough Configuration with Docker Compose on Debian
&lt;/h2&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
I do not use NVIDIA or AMD hardware in my lab. Everything NVIDIA or AMD related in this post is straight from the documention.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="nvidia-compose-example"&gt;NVIDIA Compose Example:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jellyfin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin/jellyfin:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8096:8096&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/jellyfin/config:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/media:/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nvidia&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nvidia&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;gpu]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="intelamd-compose-example"&gt;Intel/AMD Compose Example:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jellyfin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin/jellyfin:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/dev/dri/renderD128:/dev/dri/renderD128&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1000:1000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Adjust to your host user UID:GID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;render_gid&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Replace with actual host render group GID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;video_gid&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Replace with actual host video group GID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8096:8096&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/jellyfin/config:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/media:/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Grab the right group IDs from the host with &lt;code&gt;getent group render&lt;/code&gt; and &lt;code&gt;getent group video&lt;/code&gt;. Drop the numeric GID into &lt;code&gt;group_add&lt;/code&gt; in place of &lt;code&gt;render_gid&lt;/code&gt; and &lt;code&gt;video_gid&lt;/code&gt;. If you run a beefier Arc card, the same Compose file applies and you get more headroom for parallel streams.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DNMH4KQM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_b580_hu_fa4d16d6b98bd93a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_b580_hu_3cc7ffebfd0e4987.webp" width="600" height="607" alt="Sparkle Intel Arc B580 Titan" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Sparkle Intel Arc B580 Titan&lt;/strong&gt;
The Intel Arc B580 is a transcoding powerhouse, with full hardware support for AV1, HEVC, VP9, and H.264 plus 12 GB of VRAM for smooth multi-stream 4K/8K workflows. Its 160 XMX AI engines turbocharge upscaling and media conversions, making it perfect for Plex, Jellyfin, or Docker-based media servers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4oYTbOM" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Once the Compose file is in place and the GPU is wired through, the only thing left is troubleshooting whatever the first &lt;code&gt;docker compose up -d&lt;/code&gt; throws at you.&lt;/p&gt;
&lt;h2 id="resolving-common-gpu-passthrough-issues-on-debian-with-docker"&gt;Resolving Common GPU Passthrough Issues on Debian with Docker
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no available runtime&lt;/code&gt; or &lt;code&gt;could not select device driver&lt;/code&gt; means the NVIDIA Container Toolkit isn&amp;rsquo;t installed or the daemon hasn&amp;rsquo;t been restarted since you configured it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no such device /dev/dri/renderD128&lt;/code&gt; means the device nodes aren&amp;rsquo;t mapped, or the GPU drivers aren&amp;rsquo;t loaded on the host. Run &lt;code&gt;ls -l /dev/dri/&lt;/code&gt; to confirm.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Permission denied&lt;/code&gt; errors almost always trace back to missing &lt;code&gt;group_add&lt;/code&gt; for &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;video&lt;/code&gt;. Double-check the numeric GIDs match the host.&lt;/li&gt;
&lt;li&gt;After a kernel update, device numbering can shift. Use a udev rule to pin permissions, or symlink &lt;code&gt;/dev/dri/renderD128&lt;/code&gt; so the path stays stable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="verifying-gpu-access-inside-your-docker-container"&gt;Verifying GPU Access Inside Your Docker Container
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;For NVIDIA: &lt;code&gt;docker run --rm --gpus=all nvidia/cuda nvidia-smi&lt;/code&gt;. You want to see your card listed.&lt;/li&gt;
&lt;li&gt;For Intel/AMD: &lt;code&gt;docker run --rm --device /dev/dri:/dev/dri jrottenberg/ffmpeg ffmpeg -hwaccels&lt;/code&gt;. Look for &lt;code&gt;vaapi&lt;/code&gt; or &lt;code&gt;qsv&lt;/code&gt; in the output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If those work from a throwaway container, your Compose file will work too.&lt;/p&gt;
&lt;h2 id="real-world-example-jellyfin-gpu-passthrough-on-debian"&gt;Real-World Example: Jellyfin GPU Passthrough on Debian
&lt;/h2&gt;&lt;h3 id="nvidia-enabled-jellyfin"&gt;NVIDIA-enabled Jellyfin:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jellyfin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin/jellyfin:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/jellyfin/config:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/media:/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8096:8096&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nvidia&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reservations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nvidia&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;gpu]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="intel-igpu-jellyfin"&gt;Intel iGPU Jellyfin:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;jellyfin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin/jellyfin:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jellyfin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/dev/dri/renderD128:/dev/dri/renderD128&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;1000:1000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;render_gid&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;video_gid&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/jellyfin/config:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/srv/media:/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;8096:8096&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After the container is up, head into Jellyfin&amp;rsquo;s Dashboard, open Playback, and switch hardware acceleration to &lt;code&gt;VA-API&lt;/code&gt; (Intel/AMD) or &lt;code&gt;NVENC&lt;/code&gt; (NVIDIA). Set the render device to &lt;code&gt;/dev/dri/renderD128&lt;/code&gt; for VA-API. Play a 4K file from a phone or browser and watch &lt;code&gt;intel_gpu_top&lt;/code&gt; or &lt;code&gt;nvidia-smi dmon&lt;/code&gt; light up on the host. If the host stays quiet and the CPU spikes, transcoding is still landing on software.&lt;/p&gt;
&lt;h2 id="final-tips-for-installing-docker-on-debian-and-running-gpu-passthrough-on-raspberry-pi-and-home-servers"&gt;Final Tips for Installing Docker on Debian and Running GPU Passthrough on Raspberry Pi and Home Servers
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Always verify GPU functionality on the host before you touch Docker.&lt;/li&gt;
&lt;li&gt;Install Docker on Debian from the official repositories or Docker&amp;rsquo;s install script.
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/" target="_blank" rel="noopener"
&gt;My Docker install instructions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;docker run -it&lt;/code&gt; with &lt;code&gt;--gpus&lt;/code&gt; for a quick smoke test on NVIDIA before you commit to a Compose file.&lt;/li&gt;
&lt;li&gt;A Raspberry Pi 4 or 5 has no useful video encoder for modern codecs. If you want serious Jellyfin transcoding, run it on x86 with an Intel iGPU or a discrete Arc card. The NUC below is the small-footprint version of that recommendation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run those steps in order and you&amp;rsquo;ll have hardware-accelerated transcoding working inside Docker on Debian.&lt;/p&gt;
&lt;h2 id="other-resources"&gt;Other Resources
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html" target="_blank" rel="noopener"
&gt;NVIDIA Container Toolkit install guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.docker.com/config/containers/resource_constraints/#gpu" target="_blank" rel="noopener"
&gt;Docker GPU guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration" target="_blank" rel="noopener"
&gt;Jellyfin hardware acceleration docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://trac.ffmpeg.org/wiki/Hardware/VAAPI" target="_blank" rel="noopener"
&gt;FFmpeg VA-API guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://trac.ffmpeg.org/wiki/HWAccelIntro#NVENC" target="_blank" rel="noopener"
&gt;FFmpeg NVENC guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Happy transcoding with Docker.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5)&lt;/strong&gt;
Compact mini PC for lightweight servers, GPU Passthrough, Docker stacks, and VMs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Proxmox Intel GPU Passthrough: QuickSync &amp; Arc Setup Guide</title><link>https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/</link><pubDate>Fri, 15 Aug 2025 05:17:37 -0600</pubDate><guid>https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/</guid><description>&lt;img src="https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/featured_hu_51f9368263f5a0b1.webp" alt="Featured image of post Proxmox Intel GPU Passthrough: QuickSync &amp; Arc Setup Guide" /&gt;&lt;p&gt;So, you want smooth media transcoding in your Jellyfin VM running on Proxmox, and you&amp;rsquo;ve heard Intel QuickSync is the silver bullet. Or maybe you’re chasing GPU cycles for gaming, but Proxmox’s default CPU emulation isn’t cutting it. Hardware passthrough is the promise of bare-metal performance wrapped in the warm hug of virtualization.&lt;/p&gt;
&lt;p&gt;Here’s the kicker Reddit forgets to mention: halfway in, your server can black out. Silent, unresponsive, and only revivable through SSH. Think digital coma. Good times.&lt;/p&gt;
&lt;p&gt;If you’ve ever wrestled with PCIe passthrough, IOMMU groups, or found yourself frantically Googling “why is my Proxmox host dead after GPU passthrough?” with the host unreachable, welcome to the club. The membership fee? A few gray hairs and an extra helping of existential dread. If not, congrats, and read on to keep it that way.&lt;/p&gt;
&lt;p&gt;This guide walks through GPU passthrough in Proxmox, the gotchas that matter, and how to get QuickSync (or any GPU) transcoding without turning your Proxmox box into a doorstop.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Want blazing-fast media transcodes in your Jellyfin or Plex VM? GPU passthrough lets your VM access your Intel QuickSync or discrete GPU directly, with no emulation and no lag. But if you recklessly hand over your only GPU, your Proxmox host might go dark faster than your hopes during a Blue Screen of death. This guide walks you through setup, IOMMU group hell, and how to avoid turning your homelab into a brick.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;
The Arc A580 can hold its own in games, but here it’s for obliterating video streams. With support for H.264, HEVC, and full AV1 hardware encode/decode across two media engines, it crushes 20+ 1080p streams or 6–8 HDR tone-mapped 4Ks without breaking a sweat. Drop it in your media server, give Jellyfin direct VA-API access, and watch your CPU finally cool off for a bit.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-is-hardware-passthrough-really"&gt;What Is Hardware Passthrough, Really?
&lt;/h2&gt;&lt;p&gt;Think of your typical VM setup like a hotel stay. Your VM gets a comfortable room (virtualized hardware), but it shares resources with every other guest. Proxmox, our hypervisor, is the hotel manager allocating what you get and when.&lt;/p&gt;
&lt;p&gt;Hardware passthrough is like buying out an entire floor, with an express elevator. Instead of your VM politely asking Proxmox for GPU power, you hand over the hardware directly. “This is yours, use it as you like.”&lt;/p&gt;
&lt;p&gt;The payoff: your VM’s applications talk directly to the hardware, bypassing virtualization overhead. For transcoding, Intel QuickSync runs at full bare-metal speed.&lt;/p&gt;
&lt;p&gt;Here’s the thing: when you do Proxmox PCIe passthrough, you’re yanking that hardware away from the host. Was Proxmox using that GPU for the console display? Say goodbye to that output.&lt;/p&gt;
&lt;h2 id="the-iommu-groups-reality-check"&gt;The IOMMU Groups Reality Check
&lt;/h2&gt;&lt;p&gt;IOMMU groups are the rules that decide what hardware you can actually pass through.&lt;/p&gt;
&lt;p&gt;IOMMU (Input-Output Memory Management Unit) groups are like apartment buildings for PCIe devices. Everything in one group shares certain pathways. You can’t evict one tenant in isolation, you have to pass the whole group to your VM.&lt;/p&gt;
&lt;p&gt;Why does this matter? If your GPU shares an IOMMU group with, say, your network card, you can’t pass through the GPU alone. It’s all or nothing. This is where most passthrough dreams go to die.&lt;/p&gt;
&lt;p&gt;The good news? Most modern systems have sensible group layouts, especially for integrated graphics. Intel’s QuickSync, baked into the CPU, usually plays nice.&lt;/p&gt;
&lt;h2 id="pre-flight-checklist-what-you-need"&gt;Pre-Flight Checklist: What You Need
&lt;/h2&gt;&lt;p&gt;Before you tear apart a perfectly functional Proxmox setup, check these requirements:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware Requirements:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU with Intel QuickSync (think almost any modern Intel CPU)&lt;/li&gt;
&lt;li&gt;Motherboard with IOMMU/VT-d (enable in BIOS)&lt;/li&gt;
&lt;li&gt;Enough PCIe lanes (if you&amp;rsquo;re using a discrete GPU)&lt;/li&gt;
&lt;li&gt;Backup access to your Proxmox host (SSH, IPMI, or a secondary GPU)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
The Backup Access Point Crucial Detail:
Don’t pass through your only graphics output to a VM without an alternative. SSH is great, until something breaks and you need console access. IPMI is king if you have it, and a basic secondary GPU for the host is even better.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Software Prerequisites:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox VE (obviously)&lt;/li&gt;
&lt;li&gt;A guest OS that supports your hardware&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel® Core™ i5-14500 14th Generation Desktop Processor&lt;/strong&gt;
This 14th-gen i5 packs QuickSync with UHD 770 graphics, enough to power 4K → 1080p transcodes like a champ. You’ll push 10+ simultaneous 1080p streams with near-zero CPU load. Ideal for low-power, headless Proxmox boxes that run hot and quiet. No dGPU? No problem.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Be honest. If you’re here, your CPU is probably choking on media transcoding while you dream of QuickSync.&lt;/p&gt;
&lt;h2 id="what-is-gpu-passthrough-and-why-should-you-care"&gt;What Is GPU Passthrough and Why Should You Care?
&lt;/h2&gt;&lt;p&gt;GPU passthrough in proxmox means telling Proxmox to stop hogging your GPU and give it directly to your VM. This gives your VM the keys to the Ferrari while Proxmox walks home. For QuickSync, this means Jellyfin or any media workflow will fly through transcoding.&lt;/p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Once you pass that GPU through, your Proxmox host loses access. If it’s your only GPU, the host display output goes dark. Remote access via SSH (or a serial console) is essential, not optional.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="prerequisites-what-youll-need-before-diving-in"&gt;Prerequisites: What You’ll Need Before Diving In
&lt;/h2&gt;&lt;p&gt;Don’t skip the homework before shuffling hardware assignments:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU must support IOMMU:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel calls it &lt;code&gt;VT-d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AMD calls it: &lt;code&gt;AMD-Vi&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And enable it in the BIOS (often OFF by default).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Motherboard must support IOMMU groups properly:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These groups decide what you can pass through&lt;/li&gt;
&lt;li&gt;Some boards group devices nonsensically, making passthrough a pain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;You need remote access (SSH/IPMI):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Without it, losing your display means flying blind.&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/" target="_blank" rel="noopener"
&gt;My SSH Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;GPU isolation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The PCIe slot should NOT share an IOMMU group with critical components, or passthrough may not work.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
This is the quickest way to break your server.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="checking-iommu-groups-the-foundation"&gt;Checking IOMMU Groups: The Foundation
&lt;/h2&gt;&lt;p&gt;Check how your system organizes devices. On your Proxmox host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find /sys/kernel/iommu_groups/ -type l &lt;span class="p"&gt;|&lt;/span&gt; sort -V
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should look something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/0/devices/0000:00:02.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/1/devices/0000:00:00.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/2/devices/0000:00:06.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/3/devices/0000:00:14.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/3/devices/0000:00:14.2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/4/devices/0000:00:15.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/5/devices/0000:00:16.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/6/devices/0000:00:17.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/7/devices/0000:00:1c.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/8/devices/0000:00:1c.2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/9/devices/0000:00:1f.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/9/devices/0000:00:1f.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/9/devices/0000:00:1f.4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/9/devices/0000:00:1f.5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/10/devices/0000:01:00.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/11/devices/0000:02:00.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/12/devices/0000:02:00.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/sys/kernel/iommu_groups/13/devices/0000:03:00.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To better ID what these components are run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lspci -nn
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:00.0 Host bridge &lt;span class="o"&gt;[&lt;/span&gt;0600&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Device &lt;span class="o"&gt;[&lt;/span&gt;8086:4648&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 02&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:02.0 VGA compatible controller &lt;span class="o"&gt;[&lt;/span&gt;0300&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation AlderLake-S GT1 &lt;span class="o"&gt;[&lt;/span&gt;8086:4680&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 0c&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:06.0 PCI bridge &lt;span class="o"&gt;[&lt;/span&gt;0604&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation 12th Gen Core Processor PCI Express x4 Controller &lt;span class="c1"&gt;#0 [8086:464d] (rev 02)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:14.0 USB controller &lt;span class="o"&gt;[&lt;/span&gt;0c03&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH USB 3.2 Gen 2x2 XHCI Controller &lt;span class="o"&gt;[&lt;/span&gt;8086:7ae0&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:14.2 RAM memory &lt;span class="o"&gt;[&lt;/span&gt;0500&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH Shared SRAM &lt;span class="o"&gt;[&lt;/span&gt;8086:7aa7&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:15.0 Serial bus controller &lt;span class="o"&gt;[&lt;/span&gt;0c80&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH Serial IO I2C Controller &lt;span class="c1"&gt;#0 [8086:7acc] (rev 11)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:16.0 Communication controller &lt;span class="o"&gt;[&lt;/span&gt;0780&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH HECI Controller &lt;span class="c1"&gt;#1 [8086:7ae8] (rev 11)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:17.0 SATA controller &lt;span class="o"&gt;[&lt;/span&gt;0106&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH SATA Controller &lt;span class="o"&gt;[&lt;/span&gt;AHCI Mode&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;8086:7ae2&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1c.0 PCI bridge &lt;span class="o"&gt;[&lt;/span&gt;0604&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH PCI Express Root Port &lt;span class="c1"&gt;#2 [8086:7ab9] (rev 11)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1c.2 PCI bridge &lt;span class="o"&gt;[&lt;/span&gt;0604&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH PCI Express Root Port &lt;span class="o"&gt;[&lt;/span&gt;8086:7aba&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1f.0 ISA bridge &lt;span class="o"&gt;[&lt;/span&gt;0601&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Z690 Chipset LPC/eSPI Controller &lt;span class="o"&gt;[&lt;/span&gt;8086:7a84&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1f.3 Audio device &lt;span class="o"&gt;[&lt;/span&gt;0403&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S HD Audio Controller &lt;span class="o"&gt;[&lt;/span&gt;8086:7ad0&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1f.4 SMBus &lt;span class="o"&gt;[&lt;/span&gt;0c05&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH SMBus Controller &lt;span class="o"&gt;[&lt;/span&gt;8086:7aa3&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;00:1f.5 Serial bus controller &lt;span class="o"&gt;[&lt;/span&gt;0c80&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation Alder Lake-S PCH SPI Controller &lt;span class="o"&gt;[&lt;/span&gt;8086:7aa4&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 11&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;01:00.0 Non-Volatile memory controller &lt;span class="o"&gt;[&lt;/span&gt;0108&lt;span class="o"&gt;]&lt;/span&gt;: Sandisk Corp WD Black SN770 NVMe SSD &lt;span class="o"&gt;[&lt;/span&gt;15b7:5017&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 01&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;02:00.0 Ethernet controller &lt;span class="o"&gt;[&lt;/span&gt;0200&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation &lt;span class="m"&gt;82576&lt;/span&gt; Gigabit Network Connection &lt;span class="o"&gt;[&lt;/span&gt;8086:10c9&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 01&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;02:00.1 Ethernet controller &lt;span class="o"&gt;[&lt;/span&gt;0200&lt;span class="o"&gt;]&lt;/span&gt;: Intel Corporation &lt;span class="m"&gt;82576&lt;/span&gt; Gigabit Network Connection &lt;span class="o"&gt;[&lt;/span&gt;8086:10c9&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 01&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;03:00.0 Ethernet controller &lt;span class="o"&gt;[&lt;/span&gt;0200&lt;span class="o"&gt;]&lt;/span&gt;: Realtek Semiconductor Co., Ltd. RTL8125 2.5GbE Controller &lt;span class="o"&gt;[&lt;/span&gt;10ec:8125&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;rev 05&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Find your GPU in the output. Ideally, it stands alone or with non-essential devices. If grouped with essentials (USB, network), you might need to pass more than planned or use ACS override patches (advanced territory that I will not be covering).&lt;/p&gt;
&lt;p&gt;You can see my GPU on &lt;code&gt;00:02.0&lt;/code&gt;. This is my QuickSync GPU.&lt;/p&gt;
&lt;h2 id="configuring-proxmox-for-gpu-passthrough"&gt;Configuring Proxmox for GPU Passthrough
&lt;/h2&gt;&lt;p&gt;If your hardware checks out, congrats, now on to configuration:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Enable IOMMU in Proxmox:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Edit &lt;code&gt;/etc/default/grub&lt;/code&gt; and change &lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/default/grub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Intel example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;quiet intel_iommu=on iommu=pt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;AMD example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;quiet amd_iommu=on iommu=pt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Update grub with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;update-grub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;2. Load VFIO modules:&lt;/strong&gt;
Add these to &lt;code&gt;/etc/modules&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/modules
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste these in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vfio
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vfio_iommu_type1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vfio_pci
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vfio_virqfd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;3. Blacklist host GPU drivers:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For Intel iGPU (QuickSync):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blacklist i915&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/modprobe.d/blacklist.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For NVIDIA:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blacklist nouveau&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/modprobe.d/blacklist.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For AMD:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blacklist amdgpu&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/modprobe.d/blacklist.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;4. Bind GPU to VFIO:&lt;/strong&gt;
Find your GPU’s PCI ID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lspci -nn &lt;span class="p"&gt;|&lt;/span&gt; grep -E &lt;span class="s2"&gt;&amp;#34;VGA|3D|Display&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then create &lt;code&gt;/etc/modprobe.d/vfio.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /etc/modprobe.d/vfio.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this (Replace &lt;code&gt;YOUR_GUP_ID&lt;/code&gt; with your ID example &lt;code&gt;00:02.0&lt;/code&gt;):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;options vfio-pci ids=YOUR_GPU_ID
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;5. Rebuild initramfs and reboot:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;update-initramfs -u
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then reboot&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="vm-setup-the-fun-part"&gt;VM Setup: The Fun Part
&lt;/h2&gt;&lt;p&gt;In Proxmox UI:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Edit your VM → Hardware → Add PCI Device&lt;/li&gt;
&lt;li&gt;Select your GPU (and its audio function, if present)&lt;/li&gt;
&lt;li&gt;Check “All Functions” and “Primary GPU” if necessary&lt;/li&gt;
&lt;li&gt;For some VMs (Windows), you may need a matching VBIOS/ROM file&lt;/li&gt;
&lt;li&gt;Boot VM and install drivers (Linux: &lt;code&gt;intel-media-driver&lt;/code&gt; for QuickSync, or the Windows/OS driver you need)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Passing through both graphics and audio functions mimics real hardware. The “All Functions” and “Primary GPU” options help make the transition smooth, especially for picky OSes like Windows.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5)&lt;/strong&gt;
Compact mini PC for lightweight servers, GPU Passthrough, Docker stacks, and VMs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-when-things-go-sideways"&gt;Troubleshooting: When Things Go Sideways
&lt;/h2&gt;&lt;p&gt;Proxmox PCIe passthrough feels like wizardry, until it doesn’t. If you get a black screen or your VM crashes, don’t panic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Double-check driver blacklists. One typo can ruin your weekend.&lt;/li&gt;
&lt;li&gt;Confirm IOMMU is active: &lt;code&gt;dmesg | grep -e DMAR -e IOMMU&lt;/code&gt; should show enable messages.&lt;/li&gt;
&lt;li&gt;Rely on SSH or IPMI over the local console.&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;/var/log/syslog&lt;/code&gt; and &lt;code&gt;journalctl -b&lt;/code&gt; for PCI errors. VM logs live under &lt;code&gt;/var/log/pve/qemu-server/{VMID}.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Try another PCI slot or disable peripheral devices that share the GPU’s IOMMU group.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your system is completely unresponsive, comment out VFIO and GPU configurations, rebuild initramfs, and reboot. Most disasters are reversible if you don&amp;rsquo;t panic.&lt;/p&gt;
&lt;h2 id="gotchas-caveats-and-advanced-notes"&gt;Gotchas, Caveats, and Advanced Notes
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Single-GPU pass-through disables your Proxmox host’s display.&lt;/strong&gt;&lt;br&gt;
You &lt;em&gt;will&lt;/em&gt; lose all video output on the host. Recovery requires remote access or another GPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NVIDIA consumer cards may throw tantrums.&lt;/strong&gt;&lt;br&gt;
Windows “Code 43” errors can appear.&lt;br&gt;
Hyper-V spoofing in the VM args (&lt;code&gt;hidden=1,kvm=off&lt;/code&gt;) often clears them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QuickSync isn’t always present. Verify your CPU model.&lt;/strong&gt;&lt;br&gt;
Xeon CPUs may lack QuickSync even if they have integrated graphics.&lt;br&gt;
Always check official Intel docs:&lt;br&gt;
&lt;a class="link" href="https://www.intel.com/content/www/us/en/ark.html" target="_blank" rel="noopener"
&gt;Intel Product Details&lt;/a&gt;&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Or use the Quick Sync compatibility checker: pick your CPU generation and see exactly which codecs it can decode and encode in hardware.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/quicksync-compatibility/"
class="backlink-button"
target="_self"
&gt;
Check your CPU
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;LXC vs. VM passthrough are different creatures.&lt;/strong&gt;&lt;br&gt;
LXCs share hardware via device mapping (&lt;code&gt;lxc.cgroup2.devices.allow&lt;/code&gt; and &lt;code&gt;/dev/dri&lt;/code&gt; bind-mounts).&lt;br&gt;
VMs demand exclusive, full PCI assignment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EULA caveats for consumer GPUs in datacenters.&lt;/strong&gt;&lt;br&gt;
NVIDIA and AMD consumer cards often restrict use in virtualized environments, especially commercial or datacenter.&lt;/p&gt;
&lt;h2 id="common-misconceptions"&gt;Common Misconceptions
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Myth:&lt;/strong&gt; Any consumer GPU works.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reality:&lt;/strong&gt; Pro cards (Quadro/Radeon Pro) are better supported.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Myth:&lt;/strong&gt; Rebooting fixes &lt;em&gt;EVERYTHING&lt;/em&gt;.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reality:&lt;/strong&gt; Sometimes you need to completely remove and re-add the GPU or reset the PCIe slot.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Myth:&lt;/strong&gt; Passthrough is rock-solid once it boots.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reality:&lt;/strong&gt; VM restarts may need extra care (cold boots, PCIe resets).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="recovery-best-practices-and-sanity-saving"&gt;Recovery, Best Practices, and Sanity Saving
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Use multiple GPUs if possible (one for Proxmox, one for your VM).&lt;/li&gt;
&lt;li&gt;Always set up SSH or IPMI access before tinkering.&lt;/li&gt;
&lt;li&gt;Backup &lt;code&gt;/etc/pve&lt;/code&gt; before major changes.&lt;/li&gt;
&lt;li&gt;Keep copies of your IOMMU and driver blacklist configs.&lt;/li&gt;
&lt;li&gt;If locked out: Boot live USB, chroot, and reverse your changes step-by-step.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For stability:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Match your VM’s drivers to your GPU&lt;/li&gt;
&lt;li&gt;Set VM CPU type to &lt;code&gt;host&lt;/code&gt; or enable passthrough&lt;/li&gt;
&lt;li&gt;Only pass through required PCI devices&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A clean GPU passthrough setup means less time in the recovery console and more time transcoding or gaming.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;GPU passthrough on Proxmox is like taming a clever, unpredictable cat. When it works, your VM stops behaving like a VM and QuickSync (or your RTX card) chews through workloads the host CPU used to choke on. When it doesn’t, you’ll become very familiar with the recovery console.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Passthrough gives your VM direct hardware access. The host loses that hardware. Plan accordingly.&lt;/li&gt;
&lt;li&gt;Never pass through your last GPU unless you’re ready for a headless server.&lt;/li&gt;
&lt;li&gt;IOMMU groups, BIOS settings, and Proxmox module configs must all align.&lt;/li&gt;
&lt;li&gt;Back up &lt;code&gt;/etc/pve&lt;/code&gt; and your &lt;code&gt;/etc/modprobe.d/&lt;/code&gt; files before making changes.&lt;/li&gt;
&lt;li&gt;Keep SSH or IPMI working before you reboot. That’s your lifeline when the console goes dark.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you nail it, the Proxmox PCIe passthrough setup blends virtualization flexibility with raw GPU performance, and your Jellyfin or gaming VM stops fighting the hypervisor.&lt;/p&gt;
&lt;p&gt;Need more information?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://pve.proxmox.com/wiki/Pci_passthrough" target="_blank" rel="noopener"
&gt;Proxmox Passthrough Official Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/categories/basics/" target="_blank" rel="noopener"
&gt;DIYMediaServer Guides&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://forum.proxmox.com/" target="_blank" rel="noopener"
&gt;Proxmox Forum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
A Ryzen-powered beast in a mini PC shell. Dual 2.5GbE, 10GbE option, triple NVMe. Small box, big Proxmox energy.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Master the Basics - SMB Guide for Media Servers</title><link>https://diymediaserver.com/post/smb-guide/</link><pubDate>Sat, 09 Aug 2025 05:53:49 -0600</pubDate><guid>https://diymediaserver.com/post/smb-guide/</guid><description>&lt;img src="https://diymediaserver.com/post/smb-guide/featured_hu_6036b79210a499fc.webp" alt="Featured image of post Master the Basics - SMB Guide for Media Servers" /&gt;&lt;p&gt;You&amp;rsquo;ve got files. You&amp;rsquo;ve got machines. And you&amp;rsquo;re tired of playing sneakernet with USB sticks like some kind of caveman.&lt;/p&gt;
&lt;p&gt;What you want is simple. A shared folder that every device on your network can see. Windows boxes. Macs. That crusty ThinkPad running Ubuntu in the corner. Even your &amp;ldquo;smart&amp;rdquo; TV that somehow needs to stream 4K remuxes but can&amp;rsquo;t figure out basic networking.&lt;/p&gt;
&lt;p&gt;Enter Samba. It&amp;rsquo;s your network&amp;rsquo;s Swiss Army knife, part bouncer, part bartender, part DJ. And once you set it up right, it stays out of your way.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Samba turns your Linux box into a file server every device on your network can use. We&amp;rsquo;ll skip the garbage defaults and build something that streams 4K without stuttering and doesn&amp;rsquo;t require a PhD to troubleshoot.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0D22JRHZB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ugreen-NAS_hu_9b2b8c726e38b8a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ugreen-NAS_hu_7dd7553917d2893c.webp" width="600" height="353" alt="UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS&lt;/strong&gt;
UGREEN NASync DXP4800 Plus, 4-Bay NAS with an Intel Pentium Gold 8505 5-core CPU, 8GB DDR5, a 128GB built-in SSD, 2x M.2 NVMe slots, and both 10GbE and 2.5GbE ports (Diskless). This is perfect if you don&amp;rsquo;t want to DIY your NAS.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TZdbTa" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="when-smb-is-your-best-friend"&gt;When SMB Is Your Best Friend
&lt;/h2&gt;&lt;h3 id="smb-shines-in-these-scenarios"&gt;SMB shines in these scenarios:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mixed OS chaos&lt;/strong&gt;: Windows, macOS, Linux all playing nice together. No more &amp;ldquo;sorry, your OS isn&amp;rsquo;t supported.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apps that demand SMB&lt;/strong&gt;: Time Machine backups. Windows Explorer mapping. Kodi libraries. Sonos music folders. They speak SMB or they speak nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-based security&lt;/strong&gt;: You decide who gets in and what they can touch. No more &amp;ldquo;everyone has access to everything&amp;rdquo; nonsense.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dumb devices&lt;/strong&gt;: Your smart TV, IoT gadgets, and network printers don&amp;rsquo;t know NFS from a hole in the ground. But they know SMB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform development&lt;/strong&gt;: Same project folder on Windows, Mac, and Linux. No sync conflicts, no version hell.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="skip-smb-when"&gt;Skip SMB When&amp;hellip;
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux-only network&lt;/strong&gt;: NFS is faster, simpler, and doesn&amp;rsquo;t carry Windows baggage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote access&lt;/strong&gt;: SSHFS or WebDAV won&amp;rsquo;t make you cry when you&amp;rsquo;re connecting over WAN.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="install-samba-debianubuntu"&gt;Install Samba (Debian/Ubuntu)
&lt;/h2&gt;&lt;p&gt;Install the &lt;code&gt;samba&lt;/code&gt; package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install samba
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. No package maze, no dependency hell.&lt;/p&gt;
&lt;p&gt;Now set the Samba user&amp;rsquo;s password. This is &lt;strong&gt;not&lt;/strong&gt; optional:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smbpasswd -a yourusername
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;strong&gt;Why this matters&lt;/strong&gt;: Samba uses its own password database. Your Linux user exists? Great. But without &lt;code&gt;smbpasswd&lt;/code&gt;, you&amp;rsquo;re locked out. I&amp;rsquo;ve watched too many people bang their head against &amp;ldquo;authentication failed&amp;rdquo; errors because they skipped this step.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Start the services:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now smbd nmbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="a-configuration-file-that-works"&gt;A Configuration File That Works
&lt;/h2&gt;&lt;p&gt;Time to ditch the ancient defaults in &lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;. Here&amp;rsquo;s what you need.&lt;/p&gt;
&lt;h3 id="global-settings-with-large-file-transfers-and-security-in-mind"&gt;&lt;code&gt;[global]&lt;/code&gt; Settings: With Large File Transfers and Security in Mind
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;global&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;workgroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; WORKGROUP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; netbios &lt;span class="nv"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; MEDIA-SERVER
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; server &lt;span class="nv"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; %h server &lt;span class="o"&gt;(&lt;/span&gt;Samba %v&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Security that makes sense&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;security&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; user
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; map to &lt;span class="nv"&gt;guest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; Bad User
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Force modern protocols&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; server min &lt;span class="nv"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; SMB2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; server max &lt;span class="nv"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; SMB3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; client min &lt;span class="nv"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; SMB2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Encrypt everything&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; smb &lt;span class="nv"&gt;encrypt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; required
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Disable insecure features&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; lanman &lt;span class="nv"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ntlm &lt;span class="nv"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Kill anonymous access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; restrict &lt;span class="nv"&gt;anonymous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Network optimizations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; socket &lt;span class="nv"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; TCP_NODELAY &lt;span class="nv"&gt;SO_RCVBUF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;131072&lt;/span&gt; &lt;span class="nv"&gt;SO_SNDBUF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;131072&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# File transfer optimization&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; use &lt;span class="nv"&gt;sendfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Async I/O for large files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; aio &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nv"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;16384&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; aio write &lt;span class="nv"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;16384&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Reduce metadata overhead&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; strict &lt;span class="nv"&gt;allocate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Logging that helps&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; log &lt;span class="nv"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; /var/log/samba/log.%m
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; max log &lt;span class="nv"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Stop being a DNS server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; dns &lt;span class="nv"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Why these settings work&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;security = user&lt;/code&gt;: Per-user authentication. Not &amp;ldquo;everyone&amp;rsquo;s admin&amp;rdquo; madness.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map to guest = Bad User&lt;/code&gt;: Invalid usernames become guest access (if you allow it).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server min/max protocol&lt;/code&gt;: Forces SMB2/3, ditches the security nightmare that is SMB1.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dns proxy = no&lt;/code&gt;: Samba shouldn&amp;rsquo;t handle DNS. That&amp;rsquo;s your DNS server&amp;rsquo;s job.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;smb encrypt = required&lt;/code&gt;: All traffic encrypted. No plaintext passwords floating around.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restrict anonymous = 2&lt;/code&gt;: Kills guest browsing and anonymous enumeration.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lanman/ntlm auth = no&lt;/code&gt;: Forces modern authentication methods.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="share-definition-where-the-magic-happens"&gt;&lt;code&gt;[share]&lt;/code&gt; Definition: Where the Magic Happens
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;media&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;browseable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; yes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nv"&gt;only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; guest &lt;span class="nv"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; valid &lt;span class="nv"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; yourusername
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; force &lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; yourusername
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; create &lt;span class="nv"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0660&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; directory &lt;span class="nv"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0770&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Why this configuration works&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;force user/group&lt;/code&gt;: Every file gets the same owner. No more permission spaghetti.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create/directory mask&lt;/code&gt;: Sane default permissions. Files get &lt;code&gt;660&lt;/code&gt;, directories get &lt;code&gt;770&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;browseable = yes&lt;/code&gt;: Shows up in Windows Network Explorer without extra clicking.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;guest ok = no&lt;/code&gt;: Authentication required. Because security matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A share definition like this is only as fast as the pipe it rides on. If you&amp;rsquo;re still on a single 1GbE link and pushing 4K remuxes, you&amp;rsquo;re going to feel it.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;
Plug-and-play 2.5GbE PCIe card that unlocks multi-gig speeds for about $30. Works out of the box with Proxmox, Linux, and Windows. No drama, no driver hunt, faster transfers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-fix-the-usual-suspects"&gt;Troubleshooting: Fix the Usual Suspects
&lt;/h2&gt;&lt;p&gt;Check your configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo testparm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test shares without leaving the server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;smbclient -L localhost -U yourusername
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check Samba&amp;rsquo;s view of connected users:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smbstatus
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Watch logs live:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tail -f /var/log/samba/log.smbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Flush config changes without a reboot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart smbd nmbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="common-problems-fast-fixes"&gt;Common Problems, Fast Fixes
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&amp;ldquo;Access denied&amp;rdquo; but login works&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check Linux filesystem permissions&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;valid users&lt;/code&gt; matches actual usernames&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Share invisible in Windows&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;netbios name&lt;/code&gt; to &lt;code&gt;[global]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Restart both services: &lt;code&gt;sudo systemctl restart smbd nmbd&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows sometimes caches old network info. Reboot the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Slow transfers&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable &lt;code&gt;use sendfile = yes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Check your network. Gigabit wired &amp;gt; WiFi &amp;gt; carrier pigeon&lt;/li&gt;
&lt;li&gt;Large files? Enable async I/O settings above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Mount error: Protocol not supported&amp;rdquo;&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client trying to use SMB1&lt;/li&gt;
&lt;li&gt;Fix: &lt;code&gt;mount -t cifs //server/share /mnt -o vers=3.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Nuclear Option:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When nothing else works:
Stop &lt;code&gt;samba&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl stop smbd nmbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Remove samba&amp;rsquo;s cache files:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo rm /var/cache/samba/*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Start &lt;code&gt;samba&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start smbd nmbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="real-world-scenarios"&gt;Real-World Scenarios
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Media Server Hub:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Point Jellyfin, Plex, or Kodi at your SMB share. Works across containers, VMs, and bare metal. One library, every device.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Development Folder&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Same codebase accessible from your Windows IDE, Mac laptop, and Linux server. No git commits to shuffle files around.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backup Target&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Time Machine over SMB. Veeam backups. Even &lt;code&gt;rsync&lt;/code&gt; via mounted shares. Central storage that everything can hit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cheap NAS Alternative&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Old PC + big drives + Samba = network storage that doesn&amp;rsquo;t cost $800.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="smb-vs-the-alternatives"&gt;SMB vs. The Alternatives
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SMB&lt;/th&gt;
&lt;th&gt;NFS&lt;/th&gt;
&lt;th&gt;SSHFS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows native&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS native&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance (LAN)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Okay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security over WAN&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup complexity&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device compatibility&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The verdict&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SMB wins on compatibility.&lt;/li&gt;
&lt;li&gt;NFS wins on pure speed.&lt;/li&gt;
&lt;li&gt;SSHFS wins on security.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick your poison based on what matters most.&lt;/p&gt;
&lt;h2 id="the-macos-update-that-broke-everything"&gt;The macOS Update That Broke Everything
&lt;/h2&gt;&lt;p&gt;Client calls in a panic. &amp;ldquo;My media server died.&amp;rdquo; Nothing changed on his end except a macOS update.&lt;/p&gt;
&lt;p&gt;The culprit? Apple dropped SMB1 support. His Samba config was still allowing the old protocol, and macOS decided it wasn&amp;rsquo;t secure enough anymore.&lt;/p&gt;
&lt;p&gt;One line in &lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;server min &lt;span class="nv"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; SMB2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Five minutes later, his movie night was back on track.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson learned&lt;/strong&gt;: Set your protocol minimums now. Let the OS updates come. You&amp;rsquo;ll be ready.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom Line
&lt;/h2&gt;&lt;p&gt;Samba isn&amp;rsquo;t sexy. It&amp;rsquo;s not the hot new container orchestration platform. It doesn&amp;rsquo;t have a JavaScript framework named after it.&lt;/p&gt;
&lt;p&gt;But it works. It connects everything to everything else. And once you configure it right, it fades into the background and does its job.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the best kind of technology.&lt;/p&gt;
&lt;h2 id="your-network-your-rules"&gt;Your Network, Your Rules
&lt;/h2&gt;&lt;p&gt;Stop wrestling with cloud sync conflicts and USB cable hell. Set up a proper SMB share. Stream your movies without stuttering. Back up your machines to something you control.&lt;/p&gt;
&lt;p&gt;When you run your own file server, you&amp;rsquo;re sharing files and taking back control of your data. That&amp;rsquo;s worth the thirty minutes it takes to get Samba running right.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to build something that works?&lt;/strong&gt; Start with the basic config above, test it with one device, then expand from there. Your future self will thank you when everything connects on the first try.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Master the Basics - NFS Guide for Media Servers</title><link>https://diymediaserver.com/post/nfs-guide/</link><pubDate>Sat, 02 Aug 2025 07:46:39 -0600</pubDate><guid>https://diymediaserver.com/post/nfs-guide/</guid><description>&lt;img src="https://diymediaserver.com/post/nfs-guide/featured_hu_f9e13900158354c1.webp" alt="Featured image of post Master the Basics - NFS Guide for Media Servers" /&gt;&lt;p&gt;You spin up an NFS share and think you&amp;rsquo;re done. Your media server is humming, files are flowing, everything looks perfect. But here&amp;rsquo;s what you probably missed: you handed every client&amp;rsquo;s root user complete administrative access to your server. That &amp;ldquo;quick setup&amp;rdquo; you found online? It passed out master keys to anyone on your network.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t panic. We&amp;rsquo;ve all been there. Setting up NFS properly isn&amp;rsquo;t rocket science once you understand what&amp;rsquo;s happening under the hood. Let me walk you through an NFS setup that&amp;rsquo;s fast, secure, and won&amp;rsquo;t keep you up at night.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Use &lt;code&gt;root_squash&lt;/code&gt; so a remote root user can&amp;rsquo;t become your new system admin. Lock down access to specific trusted IPs, not your entire network. Always use &lt;code&gt;sync&lt;/code&gt;, because files sitting in memory aren&amp;rsquo;t saved files, they&amp;rsquo;re hopes. Never touch &lt;code&gt;no_root_squash&lt;/code&gt; unless you&amp;rsquo;d hand that client machine your banking passwords. Get your user IDs sorted across all systems, or use &lt;code&gt;all_squash&lt;/code&gt; to sidestep the whole mess.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0D22JRHZB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ugreen-NAS_hu_9b2b8c726e38b8a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ugreen-NAS_hu_7dd7553917d2893c.webp" width="600" height="353" alt="UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS&lt;/strong&gt;
UGREEN NASync DXP4800 Plus, 4-Bay NAS with an Intel Pentium Gold 8505 5-core CPU, 8GB DDR5, a 128GB built-in SSD, 2x M.2 NVMe slots, and both 10GbE and 2.5GbE ports (Diskless). This is perfect if you don&amp;rsquo;t want to DIY your NAS.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TZdbTa" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-nfs-matters-for-your-home-lab"&gt;Why NFS Matters for Your Home Lab
&lt;/h2&gt;&lt;p&gt;Network File System (NFS) is the backbone of most serious home media setups. Samba can be sluggish. FTP is a pain for media apps. NFS gives you near-native filesystem performance across your network. Your Jellyfin server, Sonarr, Radarr, and download clients can all share the same storage without fighting each other.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the catch. NFS was designed back when networks were trusted and security was someone else&amp;rsquo;s problem. It assumes every client is honest about who they are. That&amp;rsquo;s not how the real world works.&lt;/p&gt;
&lt;h2 id="understanding-nfs-security-why-your-setup-matters"&gt;Understanding NFS Security: Why Your Setup Matters
&lt;/h2&gt;&lt;p&gt;Before we get into the technical stuff, let&amp;rsquo;s talk about why NFS security matters. When you share a folder via NFS, you&amp;rsquo;re telling the network &amp;ldquo;here&amp;rsquo;s some storage you can use.&amp;rdquo; The problem? NFS trusts whatever user ID (UID) the client claims to be.&lt;/p&gt;
&lt;p&gt;Think about it. If your client machine says &amp;ldquo;hey, I&amp;rsquo;m root (UID 0), give me access,&amp;rdquo; NFS shrugs and says &amp;ldquo;sure thing, boss.&amp;rdquo; That client can now read, write, delete, and modify permissions on anything in your share. Not what you had in mind when you wanted to stream some movies.&lt;/p&gt;
&lt;p&gt;This is where &amp;ldquo;squashing&amp;rdquo; comes in. Squashing is NFS&amp;rsquo;s way of saying &amp;ldquo;I don&amp;rsquo;t care who you claim to be, you&amp;rsquo;re getting mapped to this safe user instead.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="setting-up-the-nfs-server"&gt;Setting Up the NFS Server
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Install the NFS server package:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install nfs-kernel-server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;
&lt;li&gt;Create your share directory. For media servers, something like &lt;code&gt;/media/storage&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Create the folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set the ownership of the folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R nobody:nogroup /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set the permissions on the folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod &lt;span class="m"&gt;755&lt;/span&gt; /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Why &lt;code&gt;nobody:nogroup&lt;/code&gt;? It&amp;rsquo;s a safe default owner that unprivileged processes can work with.
&lt;/div&gt;
&lt;/div&gt;
&lt;ol start="3"&gt;
&lt;li&gt;
&lt;p&gt;Configure Your Exports&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;/etc/exports&lt;/code&gt; in your favorite editor and add your share configuration:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/storage 192.168.1.0/24&lt;span class="o"&gt;(&lt;/span&gt;rw,sync,no_subtree_check,root_squash&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s what each piece does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/media/storage&lt;/code&gt;: The directory you&amp;rsquo;re sharing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.1.0/24&lt;/code&gt;: Your network subnet (adjust this to match your actual network)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rw&lt;/code&gt;: Read-write access&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sync&lt;/code&gt;: Wait for writes to complete before responding (more on this later)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no_subtree_check&lt;/code&gt;: Skip path validation for better performance&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root_squash&lt;/code&gt;: Map remote root to the &lt;code&gt;nobody&lt;/code&gt; user&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="4"&gt;
&lt;li&gt;Apply the config and start NFS. Run each of these separately:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo exportfs -ra
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart nfs-kernel-server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; nfs-kernel-server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note: The &lt;code&gt;-ra&lt;/code&gt; flag tells NFS to re-export all shares and pick up your new configuration.&lt;/p&gt;
&lt;h2 id="nfs-squash-options-your-security-toolkit"&gt;NFS Squash Options: Your Security Toolkit
&lt;/h2&gt;&lt;p&gt;This is where most people get confused. Let&amp;rsquo;s make it crystal clear.&lt;/p&gt;
&lt;h3 id="root_squash-the-sensible-default"&gt;root_squash: The Sensible Default
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/storage 192.168.1.100&lt;span class="o"&gt;(&lt;/span&gt;rw,sync,root_squash&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With &lt;code&gt;root_squash&lt;/code&gt;, any client claiming to be root (UID 0) gets mapped to the &lt;code&gt;nobody&lt;/code&gt; user. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Root can&amp;rsquo;t change file ownership&lt;/li&gt;
&lt;li&gt;Root can&amp;rsquo;t set special permissions&lt;/li&gt;
&lt;li&gt;Root can&amp;rsquo;t access files owned by other users&lt;/li&gt;
&lt;li&gt;Your server stays safe from root-level shenanigans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Use this for:&lt;/strong&gt; Pretty much everything unless you have a specific reason not to.&lt;/p&gt;
&lt;h3 id="all_squash-maximum-security-this-is-what-i-use"&gt;all_squash: Maximum Security (This is what I use)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/storage 192.168.1.100&lt;span class="o"&gt;(&lt;/span&gt;rw,sync,all_squash,anonuid&lt;span class="o"&gt;=&lt;/span&gt;1000,anongid&lt;span class="o"&gt;=&lt;/span&gt;1000&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With &lt;code&gt;all_squash&lt;/code&gt;, every user gets mapped to the anonymous user you specify. It&amp;rsquo;s like a shared account where nobody can tell who did what.&lt;/p&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;strong&gt;Use this for:&lt;/strong&gt; Public drop folders, backup destinations, or anywhere you want maximum simplicity and security.
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="no_root_squash-danger-zone"&gt;no_root_squash: Danger Zone
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/storage 192.168.1.100&lt;span class="o"&gt;(&lt;/span&gt;rw,sync,no_root_squash&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This disables squashing entirely. Remote root stays root with full privileges.&lt;/p&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
&lt;strong&gt;Use this for:&lt;/strong&gt; Honestly? Almost never in a home setup. Maybe Proxmox backup storage, or system imaging. Only use this if you&amp;rsquo;d trust that client with SSH root access to your server.
&lt;/div&gt;
&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Who&amp;rsquo;s Root on Client&lt;/th&gt;
&lt;th&gt;Permissions on Server&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Danger Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root_squash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Root becomes &lt;code&gt;nfsnobody&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limited, can&amp;rsquo;t chown/setuid&lt;/td&gt;
&lt;td&gt;Multi-user environments&lt;/td&gt;
&lt;td&gt;🟢 Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all_squash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everyone becomes &lt;code&gt;anonuid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everyone writes as same safe user&lt;/td&gt;
&lt;td&gt;Guest shares, dropzones&lt;/td&gt;
&lt;td&gt;🟡 Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no_root_squash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Root stays root&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Proxmox backups (maybe)&lt;/td&gt;
&lt;td&gt;🔴 High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="nfs-client-setup-connecting-the-pieces"&gt;NFS Client Setup: Connecting the Pieces
&lt;/h2&gt;&lt;p&gt;Now let&amp;rsquo;s get your client machines connected.&lt;/p&gt;
&lt;h3 id="install-client-tools"&gt;Install Client Tools
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install nfs-common
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="create-mount-points"&gt;Create Mount Points
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="test-your-connection"&gt;Test Your Connection
&lt;/h3&gt;&lt;p&gt;Before making anything permanent, test the mount:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -t nfs 192.168.1.5:/media/storage /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If this works, run &lt;code&gt;ls /mnt/media&lt;/code&gt; and you should see your shared content.&lt;/p&gt;
&lt;h3 id="make-it-permanent"&gt;Make It Permanent
&lt;/h3&gt;&lt;p&gt;Add this line to &lt;code&gt;/etc/fstab&lt;/code&gt; to mount automatically on boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.1.5:/media/storage /mnt/media nfs defaults,nfsvers&lt;span class="o"&gt;=&lt;/span&gt;4,hard,intr &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;
Plug-and-play 2.5GbE PCIe card that unlocks multi-gig speeds for about $30. Works out of the box with Proxmox, Linux, and Windows. No drama, faster transfers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="additional-details"&gt;Additional Details
&lt;/h2&gt;&lt;h3 id="the-sync-vs-async-debate-why-data-integrity-matters"&gt;The sync vs async Debate: Why Data Integrity Matters
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s a decision that affects both performance and safety.&lt;/p&gt;
&lt;h4 id="sync-the-safe-choice"&gt;sync: The Safe Choice
&lt;/h4&gt;&lt;p&gt;With &lt;code&gt;sync&lt;/code&gt;, your NFS server waits for each write to actually hit the disk before telling the client &amp;ldquo;yep, that&amp;rsquo;s saved.&amp;rdquo; It&amp;rsquo;s slower, but your data is safe.&lt;/p&gt;
&lt;h4 id="async-the-fast-option"&gt;async: The Fast Option
&lt;/h4&gt;&lt;p&gt;With &lt;code&gt;async&lt;/code&gt;, the server says &amp;ldquo;sure, I&amp;rsquo;ll save that&amp;rdquo; and returns immediately, even if the data is still sitting in memory. It&amp;rsquo;s faster. If your server crashes, you might lose recent writes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The verdict:&lt;/strong&gt; Use &lt;code&gt;sync&lt;/code&gt; unless you&amp;rsquo;re running benchmarks or you enjoy living dangerously. Your media library is worth the small performance hit.&lt;/p&gt;
&lt;h3 id="solving-the-uidgid-nightmare"&gt;Solving the UID/GID Nightmare
&lt;/h3&gt;&lt;p&gt;This is where many NFS setups fall apart. Your Jellyfin server runs as user ID 998 on one machine. Your download client runs as user ID 1001 on another. When they try to access the same files over NFS, permissions explode.&lt;/p&gt;
&lt;h4 id="the-manual-approach"&gt;The Manual Approach
&lt;/h4&gt;&lt;p&gt;Align user IDs across all your machines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On each client machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -u &lt;span class="m"&gt;1000&lt;/span&gt; jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -u &lt;span class="m"&gt;1000&lt;/span&gt; sonarr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo groupmod -g &lt;span class="m"&gt;1000&lt;/span&gt; media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="the-group-approach-recommended"&gt;The Group Approach (Recommended)
&lt;/h4&gt;&lt;p&gt;Create a shared group and add all your media services to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On the NFS server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo groupadd -g &lt;span class="m"&gt;2000&lt;/span&gt; mediausers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chgrp -R mediausers /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod -R &lt;span class="m"&gt;775&lt;/span&gt; /media/storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On each client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -a -G mediausers jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -a -G mediausers sonarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Everyone in the &lt;code&gt;mediausers&lt;/code&gt; group can read and write, regardless of their individual user IDs.&lt;/p&gt;
&lt;h2 id="nfsv3-vs-nfsv4-choosing-your-version"&gt;NFSv3 vs NFSv4: Choosing Your Version
&lt;/h2&gt;&lt;p&gt;Understanding the differences helps you make the right call.&lt;/p&gt;
&lt;h3 id="nfsv3-the-old-reliable"&gt;NFSv3: The Old Reliable
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Uses multiple ports (2049 plus random high ports)&lt;/li&gt;
&lt;li&gt;Separate daemons for locking and status&lt;/li&gt;
&lt;li&gt;Simpler protocol, potentially faster for basic operations&lt;/li&gt;
&lt;li&gt;Pain in the neck for firewalls&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nfsv4-the-modern-choice"&gt;NFSv4: The Modern Choice
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Single port (2049)&lt;/li&gt;
&lt;li&gt;Built-in locking and security&lt;/li&gt;
&lt;li&gt;Better support for ACLs and modern features&lt;/li&gt;
&lt;li&gt;Slightly more overhead but much easier to secure&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
Go with NFSv4 unless you have specific legacy requirements. It&amp;rsquo;s easier to firewall and generally more robust.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="mount-options-that-matter"&gt;Mount Options That Matter
&lt;/h2&gt;&lt;p&gt;The default mount options are okay. You can do better:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.1.5:/media/storage /mnt/media nfs hard,intr,nfsvers&lt;span class="o"&gt;=&lt;/span&gt;4,rsize&lt;span class="o"&gt;=&lt;/span&gt;1048576,wsize&lt;span class="o"&gt;=&lt;/span&gt;1048576,noatime &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s what each option does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hard&lt;/code&gt;: Don&amp;rsquo;t give up on network failures, keep retrying&lt;/li&gt;
&lt;li&gt;&lt;code&gt;intr&lt;/code&gt;: Allow interruption with Ctrl+C if things get stuck&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nfsvers=4&lt;/code&gt;: Force NFSv4 for better security and single-port operation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rsize/wsize=1048576&lt;/code&gt;: Use 1MB read/write buffers for better performance&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noatime&lt;/code&gt;: Don&amp;rsquo;t update access timestamps, reduces I/O overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="docker-and-nfs-the-right-way"&gt;Docker and NFS: The Right Way
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s a common mistake: mounting NFS shares directly inside Docker containers. Don&amp;rsquo;t do this. It&amp;rsquo;s unreliable and causes weird permission issues.&lt;/p&gt;
&lt;p&gt;Instead, mount NFS on your Docker host and bind-mount into containers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Mount on the host first&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount 192.168.1.5:/media/storage /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Then use in docker-compose.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - /mnt/media:/data/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; user: &lt;span class="s2"&gt;&amp;#34;1000:1000&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# Match your media user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach is more reliable and gives you better control over permissions.&lt;/p&gt;
&lt;h2 id="firewall-configuration-locking-down-access"&gt;Firewall Configuration: Locking Down Access
&lt;/h2&gt;&lt;p&gt;Out of the box, NFSv3 can open multiple ports. That&amp;rsquo;s a security nightmare. Here&amp;rsquo;s how to lock it down.&lt;/p&gt;
&lt;h3 id="for-nfsv3-if-you-must"&gt;For NFSv3 (If You Must)
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;ll need to pin the additional services to specific ports. Add this to &lt;code&gt;/etc/default/nfs-kernel-server&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RPCMOUNTDOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--manage-gids --port 32767&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;STATDOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--port 32765 --outgoing-port 32766&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then restart NFS and open the ports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart nfs-kernel-server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 192.168.1.0/24 to any port &lt;span class="m"&gt;2049&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 192.168.1.0/24 to any port 32765:32767
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="for-nfsv4-recommended"&gt;For NFSv4 (Recommended)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow from 192.168.1.0/24 to any port &lt;span class="m"&gt;2049&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="common-nfs-problems-and-how-to-fix-them"&gt;Common NFS Problems and How to Fix Them
&lt;/h2&gt;&lt;h3 id="stale-file-handles"&gt;Stale File Handles
&lt;/h3&gt;&lt;p&gt;This happens when you move or delete files on the server while clients are accessing them. The fix is usually a remount:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount 192.168.1.5:/media/storage /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To prevent this, avoid editing files directly on the NFS share. Work on files locally and move them into place when complete.&lt;/p&gt;
&lt;p&gt;For example, if you&amp;rsquo;re processing a video file, edit it in &lt;code&gt;/tmp/processing/movie.mkv&lt;/code&gt; and then &lt;code&gt;mv /tmp/processing/movie.mkv /mnt/media/movies/movie.mkv&lt;/code&gt; when done. NFS clients either see the old file or the new file, never a half-written mess that causes stale handles.&lt;/p&gt;
&lt;h3 id="performance-issues"&gt;Performance Issues
&lt;/h3&gt;&lt;p&gt;If NFS feels slow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check your network. Gigabit Ethernet is the minimum for media streaming.&lt;/li&gt;
&lt;li&gt;Verify your disk I/O isn&amp;rsquo;t the bottleneck with &lt;code&gt;iostat -x 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Try increasing rsize/wsize values in your mount options&lt;/li&gt;
&lt;li&gt;Consider NFSv4 if you&amp;rsquo;re still on v3&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="permission-denied-errors"&gt;Permission Denied Errors
&lt;/h3&gt;&lt;p&gt;Usually a UID/GID mismatch. Check:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What user/group owns the files on the server?&lt;/li&gt;
&lt;li&gt;What user/group is your client process running as?&lt;/li&gt;
&lt;li&gt;Are your squash settings allowing the access you need?&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="performance-tuning-and-benchmarking"&gt;Performance Tuning and Benchmarking
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t assume your NFS setup is fast. Measure it.&lt;/p&gt;
&lt;h3 id="basic-write-test"&gt;Basic Write Test
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dd &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/media/testfile &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="real-world-performance-testing"&gt;Real-World Performance Testing
&lt;/h3&gt;&lt;p&gt;Install &lt;code&gt;fio&lt;/code&gt; for more realistic testing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install fio
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test random read/write (simulates media streaming):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fio --name&lt;span class="o"&gt;=&lt;/span&gt;nfs-test --ioengine&lt;span class="o"&gt;=&lt;/span&gt;libaio --rw&lt;span class="o"&gt;=&lt;/span&gt;randrw --bs&lt;span class="o"&gt;=&lt;/span&gt;64k --size&lt;span class="o"&gt;=&lt;/span&gt;1G --numjobs&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; --runtime&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt; --group_reporting --filename&lt;span class="o"&gt;=&lt;/span&gt;/mnt/media/fiotest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sustained throughput above 100MB/s for 4K media streaming&lt;/li&gt;
&lt;li&gt;Low latency (under 10ms for most operations)&lt;/li&gt;
&lt;li&gt;No dramatic performance drops under load&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="advanced-security-beyond-basic-ip-filtering"&gt;Advanced Security: Beyond Basic IP Filtering
&lt;/h2&gt;&lt;h3 id="using-hostsallow-and-hostsdeny"&gt;Using hosts.allow and hosts.deny
&lt;/h3&gt;&lt;p&gt;Add an extra layer of access control. Add this to &lt;code&gt;/etc/hosts.allow&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpcbind mountd nfsd statd lockd rquotad : 192.168.1.0/24
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add this to &lt;code&gt;/etc/hosts.deny&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rpcbind mountd nfsd statd lockd rquotad : ALL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="network-segmentation"&gt;Network Segmentation
&lt;/h3&gt;&lt;p&gt;Consider putting your NFS traffic on a dedicated VLAN or subnet. It limits the blast radius if something goes wrong and makes monitoring easier.&lt;/p&gt;
&lt;h2 id="your-nfs-pre-flight-checklist"&gt;Your NFS Pre-Flight Checklist
&lt;/h2&gt;&lt;p&gt;Before you call your setup &amp;ldquo;done,&amp;rdquo; verify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Squash options are configured for your security needs&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; You&amp;rsquo;re using &lt;code&gt;sync&lt;/code&gt; for data safety&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;code&gt;no_subtree_check&lt;/code&gt; is enabled for performance&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; IP restrictions are tight and tested from allowed and denied hosts&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Firewall rules only allow access from trusted networks&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; User IDs are aligned or properly squashed&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Docker containers use host bind mounts rather than direct NFS mounts&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Mount options include &lt;code&gt;hard,intr,nfsvers=4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; You&amp;rsquo;ve tested normal operations and permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wrapping-up-nfs-that-works"&gt;Wrapping Up: NFS That Works
&lt;/h2&gt;&lt;p&gt;Anyone can copy-paste some commands and get NFS technically functioning. There&amp;rsquo;s a big difference between &amp;ldquo;technically working&amp;rdquo; and rock-solid reliable. One approach gets you streaming tonight and leaves you troubleshooting permission disasters next month. The other gets you streaming tonight and still working flawlessly two years from now.&lt;/p&gt;
&lt;p&gt;The security stuff is insurance. Those squash settings and firewall rules seem like overkill until the day they save you from a client machine that got compromised or misconfigured. Your media server should be boring in the best possible way. It works, day after day, without drama.&lt;/p&gt;
&lt;p&gt;When NFS is set up right, you forget it exists. Your Jellyfin scans run smoothly, your download clients move files without hiccups, and you never get woken up by permission errors. That&amp;rsquo;s not luck. That&amp;rsquo;s good engineering.&lt;/p&gt;
&lt;p&gt;Build it once, build it right, then move on to the next project in your lab.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Jellyfin QuickSync in a Proxmox Unprivileged LXC (Working Setup)</title><link>https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/</link><pubDate>Sat, 26 Jul 2025 07:46:41 -0600</pubDate><guid>https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/</guid><description>&lt;img src="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/featured_hu_be043d68a14503f9.webp" alt="Featured image of post Jellyfin QuickSync in a Proxmox Unprivileged LXC (Working Setup)" /&gt;&lt;h2 id="why-everyone-gets-this-wrong"&gt;Why Everyone Gets This Wrong
&lt;/h2&gt;&lt;p&gt;People treat unprivileged LXC like it&amp;rsquo;s cursed black magic. &amp;ldquo;You can&amp;rsquo;t do NFS!&amp;rdquo; &amp;ldquo;GPU passthrough is impossible!&amp;rdquo; &amp;ldquo;Use a VM, it&amp;rsquo;s easier!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complete bullshit.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You absolutely can run Jellyfin with Intel QuickSync transcoding in an unprivileged LXC container. The problem isn&amp;rsquo;t the technology, it&amp;rsquo;s that most tutorials skip the crucial details or rely on hacky workarounds that break on the first system update.&lt;/p&gt;
&lt;p&gt;This guide walks you through the &lt;em&gt;proper&lt;/em&gt; way to set up hardware-accelerated Jellyfin transcoding in an unprivileged LXC on Proxmox. No sketchy scripts. No Snap packages. No privileged containers. Clean, secure transcoding.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Yes, Intel QuickSync works in unprivileged LXC. All you need is to give the container access to the GPU, map its group IDs to match the host, install Intel drivers and Jellyfin using APT (not Snap), and flip the hardware transcoding switch in Jellyfin. The result? A secure, lightweight setup that chews through simultaneous 1080p streams without breaking a sweat.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-vms-are-overkill-for-this-job"&gt;Why VMs Are Overkill for This Job
&lt;/h2&gt;&lt;p&gt;First, let&amp;rsquo;s destroy the lazy &amp;ldquo;use a VM instead&amp;rdquo; argument once and for all.&lt;/p&gt;
&lt;h3 id="the-resource-waste-is-criminal"&gt;The Resource Waste is Criminal
&lt;/h3&gt;&lt;p&gt;Running Jellyfin in a VM means you&amp;rsquo;re literally wasting resources you paid for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Memory: 512MB-1GB VM overhead vs. 10-20MB for LXC&lt;/li&gt;
&lt;li&gt;Storage: 12-20GB VM footprint vs. 400-800MB container&lt;/li&gt;
&lt;li&gt;Boot time: 30-60 seconds vs. 2-5 seconds&lt;/li&gt;
&lt;li&gt;Backups: 10-20GB snapshots vs. 200-500MB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On a typical 16GB homelab box, that VM overhead costs you 2-3 additional services you could be running.&lt;/p&gt;
&lt;h3 id="gpu-passthrough-complexity"&gt;GPU Passthrough Complexity
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VM route:&lt;/strong&gt; VFIO setup, IOMMU groups, driver blacklisting, potential single-GPU nightmares.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LXC route:&lt;/strong&gt; Map device nodes. Done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every disk I/O and network packet in a VM goes through unnecessary virtualization layers. LXC gives you direct host kernel access with zero translation overhead.&lt;/p&gt;
&lt;h3 id="the-easier-myth"&gt;The &amp;ldquo;Easier&amp;rdquo; Myth
&lt;/h3&gt;&lt;p&gt;People claim VMs are &amp;ldquo;easier&amp;rdquo; because they&amp;rsquo;re familiar. That&amp;rsquo;s not easier. That&amp;rsquo;s lazy.
You are wasting resources, creating a more complex GPU passthrough, larger backups, and full OS maintenance overhead for zero benefit. VMs make sense for different kernels, untrusted workloads, or legacy apps that need system control.
For a media server? Not so much.&lt;/p&gt;
&lt;h3 id="the-real-reason"&gt;The Real Reason
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Fear.&lt;/strong&gt; Fear of learning device mapping. Fear of doing things better instead of familiar.
This guide eliminates that fear. Once you understand LXC device passthrough, you&amp;rsquo;ll wonder why you ever considered wasting resources on a VM for simple application hosting.&lt;/p&gt;
&lt;h2 id="prerequisites-what-you-need"&gt;Prerequisites: What You Need
&lt;/h2&gt;&lt;h3 id="hardware-requirements"&gt;Hardware Requirements
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Intel 8th gen CPU or newer with integrated graphics (Coffee Lake+)&lt;/li&gt;
&lt;li&gt;QuickSync support enabled in BIOS&lt;/li&gt;
&lt;li&gt;Proxmox VE 8.0+ host&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0CQ27H8VY"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel-i5-14500_hu_3d44066ac583f5e3.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel-i5-14500_hu_72705be001e04c0a.webp" width="600" height="661" alt="Intel® Core™ i5-14500 14th Generation Desktop Processor" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Intel® Core™ i5-14500 14th Generation Desktop Processor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Forget GPUs. This 14th-gen i5 packs QuickSync with UHD 770 graphics, enough to power 4K → 1080p transcodes like a champ. You’ll push 10+ simultaneous 1080p streams with near-zero CPU load. Ideal for low-power, headless Proxmox boxes that run hot and quiet. No dGPU? No problem.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0hN5fvVV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="supported-codecs"&gt;Supported Codecs
&lt;/h3&gt;&lt;p&gt;Intel QuickSync can hardware-accelerate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;H.264&lt;/strong&gt; (AVC) - encode/decode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;H.265&lt;/strong&gt; (HEVC) - encode/decode (9th gen+)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VP9&lt;/strong&gt; - decode only (some newer CPUs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AV1&lt;/strong&gt; - decode only (12th gen+)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Not sure what your exact generation can transcode? The Quick Sync compatibility checker lists decode and encode support per codec, including whether you get 4K HDR tone mapping.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/quicksync-compatibility/"
class="backlink-button"
target="_self"
&gt;
Check your CPU
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="alert alert-danger"&gt;
&lt;span class="alert-icon"&gt;❌&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Danger:&lt;/strong&gt;
&lt;p&gt;This is an in-depth topic.&lt;/p&gt;
&lt;p&gt;Any missed or skipped details could cause QuickSync transcoding to fail.&lt;/p&gt;
&lt;p&gt;All commands and confs have been tested and re-tested to ensure everything is accurate.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-1-verify-your-hardware-setup"&gt;Step 1: Verify Your Hardware Setup
&lt;/h2&gt;&lt;p&gt;Before diving into container configuration, confirm your hardware is ready.&lt;/p&gt;
&lt;h3 id="check-igpu-detection"&gt;Check iGPU Detection
&lt;/h3&gt;&lt;p&gt;On your Proxmox host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /dev/dri/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;crw-rw---- &lt;span class="m"&gt;1&lt;/span&gt; root video 226, &lt;span class="m"&gt;0&lt;/span&gt; card0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;crw-rw---- &lt;span class="m"&gt;1&lt;/span&gt; root render 226, &lt;span class="m"&gt;128&lt;/span&gt; renderD128
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;card0&lt;/code&gt; - Display interface (major:minor = 226:0)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;renderD128&lt;/code&gt; - Render interface for compute (major:minor = 226:128)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="understanding-device-majorminor-numbers"&gt;Understanding Device Major:Minor Numbers
&lt;/h3&gt;&lt;p&gt;When you see &lt;code&gt;226:0&lt;/code&gt; and &lt;code&gt;226:128&lt;/code&gt; in the GPU device configuration, these aren&amp;rsquo;t random numbers, they&amp;rsquo;re part of Linux&amp;rsquo;s device identification system. Understanding them is crucial for GPU passthrough because you need to grant the container permission to access these specific device numbers.&lt;/p&gt;
&lt;h4 id="what-the-hell-are-majorminor-device-numbers"&gt;What the Hell Are Major:Minor Device Numbers?
&lt;/h4&gt;&lt;p&gt;In Linux, every hardware device is represented by a file in &lt;code&gt;/dev/&lt;/code&gt;. But don&amp;rsquo;t get too excited, are not files you can open and modify. They are more like hotline numbers that the kernel uses to dial up the right hardware driver.&lt;/p&gt;
&lt;h4 id="each-device-file-has-two-id-numbers"&gt;Each device file has two ID numbers:
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Major Number: Identifies the device driver/subsystem&lt;/li&gt;
&lt;li&gt;Minor Number: Identifies the specific device within that subsystem&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="think-of-it-like-a-phone-system"&gt;Think of it like a phone system:
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Major number = Area code (which phone company/region)&lt;/li&gt;
&lt;li&gt;Minor number = Local number (which specific phone)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="load-intel-graphics-driver"&gt;Load Intel Graphics Driver
&lt;/h3&gt;&lt;p&gt;Ensure the i915 kernel module is loaded:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsmod &lt;span class="p"&gt;|&lt;/span&gt; grep i915
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Load if missing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;modprobe i915
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make persistent across reboots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;i915&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/modules
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="verify-quicksync-capability"&gt;Verify QuickSync Capability
&lt;/h3&gt;&lt;p&gt;Install tools if not present:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install intel-gpu-tools vainfo intel-media-va-driver-non-free
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check available encoders&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vainfo &lt;span class="p"&gt;|&lt;/span&gt; grep -i enc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for entries like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileH264Main : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileH264High : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileJPEGBaseline : VAEntrypointEncPicture
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileH264ConstrainedBaseline: VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCMain : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCMain10 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileVP9Profile0 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileVP9Profile1 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileVP9Profile2 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileVP9Profile3 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCMain444 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCMain444_10 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCSccMain : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCSccMain10 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCSccMain444 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; VAProfileHEVCSccMain444_10 : VAEntrypointEncSliceLP
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you don&amp;rsquo;t see these, your CPU might not support QuickSync or it&amp;rsquo;s disabled in BIOS.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;&lt;br&gt;
The Arc A580 can hold its own in games, but here it’s for obliterating video streams. With support for H.264, HEVC, and full AV1 hardware encode/decode across two media engines, it crushes 20+ 1080p streams or 6–8 HDR tone-mapped 4Ks without breaking a sweat. Drop it in your media server, give Jellyfin direct VA-API access, and watch your CPU finally cool off for a bit.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-2-create-the-lxc-container"&gt;Step 2: Create the LXC Container
&lt;/h2&gt;&lt;p&gt;In Proxmox web interface:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create CT&lt;/strong&gt; → Use Ubuntu 24.04 LTS template (Jellyfin prefers Ubuntu over Debian)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep &amp;ldquo;Unprivileged&amp;rdquo; checked&lt;/strong&gt; (this is crucial)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resources&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;CPU: 2-4 cores&lt;/li&gt;
&lt;li&gt;RAM: 2GB minimum, 4GB recommended&lt;/li&gt;
&lt;li&gt;Disk: 80GB+ for Jellyfin metadata and cache&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network&lt;/strong&gt;: Bridge to your main network&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t start the container yet&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="step-3-configure-device-access"&gt;Step 3: Configure Device Access
&lt;/h2&gt;&lt;p&gt;This is where the magic happens. Edit &lt;code&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt; and add:&lt;/p&gt;
&lt;p&gt;Allow access to DRI devices:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;lxc.cgroup2.devices.allow: c 226:0 rwm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;lxc.cgroup2.devices.allow: c 226:128 rwm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Mount DRI devices into container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;lxc.mount.entry: /dev/dri/ dev/dri/ none bind,optional,create&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dir&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="what-these-lines-do"&gt;What These Lines Do
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lxc.cgroup2.devices.allow&lt;/code&gt; - Grants permission to access specific device nodes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lxc.mount.entry&lt;/code&gt; - Bind mounts the entire &lt;code&gt;/dev/dri&lt;/code&gt; directory into the container&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-4-handle-group-id-mapping-critical"&gt;Step 4: Handle Group ID Mapping (Critical!)
&lt;/h2&gt;&lt;p&gt;This step trips up 90% of people attempting GPU passthrough. Here&amp;rsquo;s why. Unprivileged LXC containers use user namespaces to isolate processes, which means group IDs inside the container don&amp;rsquo;t directly correspond to group IDs on the host. Without proper mapping, your container processes can&amp;rsquo;t access the GPU devices even if the device files are present.&lt;/p&gt;
&lt;h3 id="understanding-user-namespaces-and-id-mapping"&gt;Understanding User Namespaces and ID Mapping
&lt;/h3&gt;&lt;p&gt;When Proxmox creates an unprivileged container, it maps container user/group IDs to a range of IDs on the host system. By default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container UID/GID 0 (root) → Host UID/GID 100000&lt;/li&gt;
&lt;li&gt;Container UID/GID 1 → Host UID/GID 100001&lt;/li&gt;
&lt;li&gt;Container UID/GID 1000 → Host UID/GID 101000&lt;/li&gt;
&lt;li&gt;And so on&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means when a process inside the container tries to access &lt;code&gt;/dev/dri/renderD128&lt;/code&gt; (owned by group ID &lt;code&gt;104&lt;/code&gt; on the host), the kernel sees it as an access attempt from group ID &lt;code&gt;100104&lt;/code&gt;, which doesn&amp;rsquo;t exist and has no permissions.&lt;/p&gt;
&lt;h3 id="the-id-mapping-strategy"&gt;The ID Mapping Strategy
&lt;/h3&gt;&lt;p&gt;We need to create &amp;ldquo;holes&amp;rdquo; in the default mapping to let specific container group IDs map directly to host group IDs. Think of it like creating bridges between the container and host for specific groups while keeping everything else isolated. This is similar to how firewall rules work.&lt;/p&gt;
&lt;h3 id="find-the-host-render-group-id"&gt;Find the Host Render Group ID
&lt;/h3&gt;&lt;p&gt;On the Proxmox host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -n /dev/dri/renderD128
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;crw-rw---- &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;104&lt;/span&gt; 226, &lt;span class="m"&gt;128&lt;/span&gt; Nov &lt;span class="m"&gt;15&lt;/span&gt; 10:30 /dev/dri/renderD128
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Breaking this down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;crw-rw----&lt;/code&gt; = Character device with read/write for owner/group&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt; = Owner UID (root)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;104&lt;/code&gt; = Group ID that owns the device (usually render group)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;226, 128&lt;/code&gt; = Major:minor device numbers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;104&lt;/code&gt; is what we need to remember. This is the host group ID we need to map later.&lt;/p&gt;
&lt;h3 id="standardize-gpu-device-ownership"&gt;Standardize GPU Device Ownership
&lt;/h3&gt;&lt;p&gt;Instead of dealing with potentially different group IDs for different GPU devices, let&amp;rsquo;s ensure both GPU devices use the same group:&lt;/p&gt;
&lt;p&gt;Change the group for &lt;code&gt;card0&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chgrp render /dev/dri/card0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make it persistent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SUBSYSTEM==&amp;#34;drm&amp;#34;, KERNEL==&amp;#34;card0&amp;#34;, GROUP=&amp;#34;render&amp;#34;, MODE=&amp;#34;0660&amp;#34;&amp;#39;&lt;/span&gt; &amp;gt; /etc/udev/rules.d/99-render.rules
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;udevadm control --reload-rules &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; udevadm trigger
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now both &lt;code&gt;card0&lt;/code&gt; and &lt;code&gt;renderD128&lt;/code&gt; should be owned by the same group ID (usually &lt;code&gt;104&lt;/code&gt; for render).&lt;/p&gt;
&lt;h3 id="create-user-in-container"&gt;Create User in Container
&lt;/h3&gt;&lt;p&gt;Start the container and enter the console to create the Jellyfin user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;adduser jellyfin --system --group --home /var/lib/jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create the group &lt;code&gt;render&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;groupadd -g &lt;span class="m"&gt;993&lt;/span&gt; render
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the &lt;code&gt;render&lt;/code&gt; group to the &lt;code&gt;Jellyfin&lt;/code&gt; user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;usermod -aG render,media jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="why-gid-993"&gt;Why GID 993?
&lt;/h3&gt;&lt;p&gt;We&amp;rsquo;re going to map container GID &lt;code&gt;993&lt;/code&gt; to host GID &lt;code&gt;104&lt;/code&gt; (the render group).&lt;br&gt;
The choice of 993 is arbitrary. It only needs to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Available in the container (not already used)&lt;/li&gt;
&lt;li&gt;Consistent with our mapping configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Show the group IDs of the user &lt;code&gt;jellyfin&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make sure the &lt;code&gt;993&lt;/code&gt; render group is listed.&lt;/p&gt;
&lt;h3 id="configure-id-mapping"&gt;Configure ID Mapping
&lt;/h3&gt;&lt;p&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
This assumes you&amp;rsquo;re using the group &lt;code&gt;media&lt;/code&gt; with an ID of &lt;code&gt;1001&lt;/code&gt; to access your media.&lt;br&gt;
If yours is different, replace any references to &lt;code&gt;media&lt;/code&gt; and &lt;code&gt;1001&lt;/code&gt; with your own.
&lt;/div&gt;
&lt;/div&gt;
Stop the container and edit &lt;code&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;:&lt;br&gt;
Map container UIDs/GIDs to host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;993&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;993&lt;/span&gt; &lt;span class="m"&gt;104&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;994&lt;/span&gt; &lt;span class="m"&gt;100994&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1002&lt;/span&gt; &lt;span class="m"&gt;101002&lt;/span&gt; &lt;span class="m"&gt;64534&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="detailed-breaking-down-each-mapping-line"&gt;Detailed Breaking Down Each Mapping Line:
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;lxc.idmap: u 0 100000 65536&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maps all container UIDs (user IDs) normally&lt;/li&gt;
&lt;li&gt;Container UID 0 → Host UID 100000&lt;/li&gt;
&lt;li&gt;Maps 65536 UIDs total (standard range)&lt;/li&gt;
&lt;li&gt;This line handles all user accounts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: g 0 100000 993&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maps container GIDs 0-992 to host GIDs 100000-100992&lt;/li&gt;
&lt;li&gt;This is the &amp;ldquo;normal&amp;rdquo; mapping for system groups&lt;/li&gt;
&lt;li&gt;Stops at 992 to leave room for our special mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: g 993 104 1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is the critical line.&lt;/li&gt;
&lt;li&gt;Maps container GID 993 → host GID 104 (render group)&lt;/li&gt;
&lt;li&gt;Only maps 1 GID (this specific group)&lt;/li&gt;
&lt;li&gt;This creates our bridge to GPU device access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: g 994 100994 7&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Resumes normal mapping for GIDs 994-1000&lt;/li&gt;
&lt;li&gt;Maps to host GIDs 100994-101000&lt;/li&gt;
&lt;li&gt;Fills the gap between our special mapping and media group&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: g 1001 1001 1&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maps container GID 1001 → host GID 1001 (media group)&lt;/li&gt;
&lt;li&gt;Assumes your media files are owned by GID 1001&lt;/li&gt;
&lt;li&gt;Adjust this to match your actual media group ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;lxc.idmap: g 1002 101002 64534&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Resumes normal mapping for all remaining GIDs&lt;/li&gt;
&lt;li&gt;Maps container GIDs 1002-65535 → host GIDs 101002-165535&lt;/li&gt;
&lt;li&gt;Handles any additional groups that might be created&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="update-host-subordinate-gids"&gt;Update Host Subordinate GIDs
&lt;/h3&gt;&lt;p&gt;On the Proxmox host add the &lt;code&gt;render&lt;/code&gt; and &lt;code&gt;media&lt;/code&gt; groups to the &lt;code&gt;subgid&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root:104:1&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;root:1001:1&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; /etc/subgid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells the system that the root user (which manages LXC containers) can map:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host GID &lt;code&gt;104&lt;/code&gt; (render group) into containers&lt;/li&gt;
&lt;li&gt;Host GID &lt;code&gt;1001&lt;/code&gt; (media group) into containers&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-5-install-software-stack"&gt;Step 5: Install Software Stack
&lt;/h2&gt;&lt;p&gt;Start the container and install the required packages:&lt;/p&gt;
&lt;p&gt;Update package lists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Install Intel GPU drivers and tools&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install intel-gpu-tools intel-media-va-driver libdrm-intel1 vainfo curl gnupg software-properties-common -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add Jellyfin repository:&lt;/p&gt;
&lt;p&gt;Pull the gpg key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key &lt;span class="p"&gt;|&lt;/span&gt; gpg --dearmor -o /usr/share/keyrings/jellyfin-archive-keyring.gpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the Repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [signed-by=/usr/share/keyrings/jellyfin-archive-keyring.gpg] https://repo.jellyfin.org/ubuntu &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release -cs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main&amp;#34;&lt;/span&gt; &amp;gt; /etc/apt/sources.list.d/jellyfin.list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Install Jellyfin:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt install jellyfin -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="why-not-snapflatpakdocker"&gt;Why Not Snap/Flatpak/Docker?
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Snap&lt;/strong&gt;: Broken device access due to confinement&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flatpak&lt;/strong&gt;: Similar sandboxing issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;: Adds unnecessary complexity to device mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;APT packages have proper system integration and device access.&lt;/p&gt;
&lt;h2 id="step-6-verify-device-access"&gt;Step 6: Verify Device Access
&lt;/h2&gt;&lt;h3 id="check-device-permissions"&gt;Check Device Permissions
&lt;/h3&gt;&lt;p&gt;Inside the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -la /dev/dri/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;crw-rw---- 1 nobody render 226, 0 card0
crw-rw---- 1 nobody render 226, 128 renderD128
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If the group isn&amp;rsquo;t &lt;code&gt;render&lt;/code&gt;, you need to check your:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lxc.mount.entry&lt;/code&gt; and/or &lt;code&gt;lxc.idmap&lt;/code&gt; entries in the LXC conf file.&lt;/p&gt;
&lt;h3 id="test-hardware-acceleration"&gt;Test Hardware Acceleration
&lt;/h3&gt;&lt;p&gt;Read Test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u jellyfin &lt;span class="nb"&gt;test&lt;/span&gt; -r /dev/dri/renderD128 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Readable&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Write Test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo -u jellyfin &lt;span class="nb"&gt;test&lt;/span&gt; -w /dev/dri/renderD128 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Writable&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-7-configure-jellyfin"&gt;Step 7: Configure Jellyfin
&lt;/h2&gt;&lt;h3 id="access-web-interface"&gt;Access Web Interface
&lt;/h3&gt;&lt;p&gt;Navigate to &lt;code&gt;http://&amp;lt;container-ip&amp;gt;:8096&lt;/code&gt; and complete the initial setup wizard.&lt;/p&gt;
&lt;h3 id="enable-hardware-acceleration"&gt;Enable Hardware Acceleration
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; → &lt;strong&gt;Playback&lt;/strong&gt; → &lt;strong&gt;Transcoding&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware acceleration&lt;/strong&gt;: Intel QuickSync (QSV)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable hardware decoding for&lt;/strong&gt;: H264, HEVC, VP9 (as supported)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable hardware encoding&lt;/strong&gt;: Yes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable VPP Tone mapping&lt;/strong&gt;: Yes (for HDR content)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="advanced-settings"&gt;Advanced Settings
&lt;/h3&gt;&lt;p&gt;For better performance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Allow encoding in HEVC format&lt;/strong&gt;: Yes (if supported)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transcoding thread count&lt;/strong&gt;: Auto&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0CLTNC6V6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="ASRock Mini-Desktop Computer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Need a Mini Server?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The DeskMini B760 is a compact and powerful barebone system perfect for homelab use. It supports 14th Gen Intel CPUs, dual DDR4 RAM up to 64GB, and fast storage via M.2 slots plus dual 2.5&amp;quot; drive bays. It&amp;rsquo;s ideal for running lightweight VMs and/or containers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4kVe2jP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="step-8-test-hardware-transcoding"&gt;Step 8: Test Hardware Transcoding
&lt;/h2&gt;&lt;h3 id="force-transcoding-test"&gt;Force Transcoding Test
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Upload a high-bitrate H.264/HEVC video to Jellyfin&lt;/li&gt;
&lt;li&gt;Start playback and immediately change quality to force transcoding&lt;/li&gt;
&lt;li&gt;On the Proxmox host (not in container) run:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;intel_gpu_top
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see spikes in the &lt;strong&gt;Video&lt;/strong&gt; engine usage during transcoding.&lt;/p&gt;
&lt;h3 id="browser-verification"&gt;Browser Verification
&lt;/h3&gt;&lt;p&gt;In Chrome/Edge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to &lt;code&gt;chrome://media-internals/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Start playing your test video&lt;/li&gt;
&lt;li&gt;Look for:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;video_codec: h264&lt;/code&gt; or &lt;code&gt;hevc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hardwareAccelerated: true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="jellyfin-dashboard"&gt;Jellyfin Dashboard
&lt;/h3&gt;&lt;p&gt;Check &lt;strong&gt;Dashboard&lt;/strong&gt; → &lt;strong&gt;Activity&lt;/strong&gt; for active transcodes. Hardware transcoding shows much lower CPU usage than software.&lt;/p&gt;
&lt;h2 id="troubleshooting-guide"&gt;Troubleshooting Guide
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Symptoms&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No /dev/dri in container&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing device files&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;lxc.mount.entry&lt;/code&gt; in config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Permission denied on GPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Cannot access /dev/dri/renderD128&amp;rdquo;&lt;/td&gt;
&lt;td&gt;Fix GID mapping or group membership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;vainfo fails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;libva error&amp;rdquo; or crashes&lt;/td&gt;
&lt;td&gt;Normal in LXC - test via Jellyfin instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU still transcoding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High CPU usage during playback&lt;/td&gt;
&lt;td&gt;Enable QSV in Jellyfin playback settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transcoding fails entirely&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playback errors or fallback to direct play&lt;/td&gt;
&lt;td&gt;Check Jellyfin logs for codec support issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU owned by wrong group&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Device shows render/video instead of jellyfin&lt;/td&gt;
&lt;td&gt;Map to correct host group ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="advanced-debugging"&gt;Advanced Debugging
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Check Jellyfin logs:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tail -f /var/log/jellyfin/jellyfin.log
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Verify codec support:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ffmpeg -hide_banner -encoders &lt;span class="p"&gt;|&lt;/span&gt; grep qsv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="performance-expectations"&gt;Performance Expectations
&lt;/h2&gt;&lt;h3 id="transcoding-capacity"&gt;Transcoding Capacity
&lt;/h3&gt;&lt;p&gt;Intel QuickSync can typically handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;8th-10th gen&lt;/strong&gt;: 4-6 simultaneous 1080p H.264 transcodes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;11th gen+&lt;/strong&gt;: 6-8 simultaneous 1080p transcodes, 2-3 4K HEVC&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;12th gen+&lt;/strong&gt;: 8-10 simultaneous 1080p, 3-4 4K transcodes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="quality-considerations"&gt;Quality Considerations
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Hardware encoding produces slightly larger files than software (x264)&lt;/li&gt;
&lt;li&gt;Quality is excellent for streaming but may not match software for archival&lt;/li&gt;
&lt;li&gt;HEVC hardware encoding (if available) provides better efficiency than H.264&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="additional-optimizations"&gt;Additional Optimizations
&lt;/h2&gt;&lt;h3 id="performance-tweaks"&gt;Performance Tweaks
&lt;/h3&gt;&lt;h4 id="memory-tuning"&gt;Memory Tuning
&lt;/h4&gt;&lt;p&gt;For 4K transcoding, increase container RAM:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;memory: 4096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="cpu-priority"&gt;CPU Priority
&lt;/h4&gt;&lt;p&gt;Give transcoding higher priority:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;cores: 4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;cpulimit: 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;cpuunits: 1024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-about-nfs"&gt;What About NFS?
&lt;/h2&gt;&lt;p&gt;Since we&amp;rsquo;re on the topic of &amp;ldquo;impossible&amp;rdquo; things in unprivileged LXC:&lt;/p&gt;
&lt;p&gt;NFS works perfectly fine. Mount it on the host, then bind mount into the container:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Host:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -t nfs nas.local:/volume1/media /mnt/nas-media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Container config:&lt;/strong&gt;
In &lt;code&gt;/etc/pve/lxc/CTID.conf&lt;/code&gt; add:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;mp0: /mnt/nas-media,mp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/mnt/media&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="security-benefits"&gt;Security Benefits
&lt;/h2&gt;&lt;p&gt;This setup provides several security advantages over alternatives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Process isolation&lt;/strong&gt;: Jellyfin runs in its own namespace&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited privilege&lt;/strong&gt;: No root access to host system&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource limits&lt;/strong&gt;: CPU/memory can be strictly controlled&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network isolation&lt;/strong&gt;: Can be restricted to specific VLANs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal attack surface&lt;/strong&gt;: Only required devices are exposed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Unprivileged LXC containers aren&amp;rsquo;t the limitation. Lack of understanding is. With proper device mapping and group management, you get the security benefits of containerization with near-native hardware performance.&lt;/p&gt;
&lt;p&gt;This setup gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Secure, unprivileged execution&lt;/li&gt;
&lt;li&gt;Direct GPU access without overhead&lt;/li&gt;
&lt;li&gt;Professional-grade media streaming&lt;/li&gt;
&lt;li&gt;Easy maintenance and updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stop settling for bloated VMs or dangerous privileged containers. Master the mappings and run Jellyfin the right way.&lt;/p&gt;
&lt;h2 id="next-steps"&gt;Next Steps
&lt;/h2&gt;&lt;p&gt;Want to level up further? Consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Remote storage&lt;/strong&gt;: Set up NFS/SMB mounts for media libraries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reverse proxy&lt;/strong&gt;: Add Nginx/Traefik for HTTPS and custom domains&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup strategy&lt;/strong&gt;: Implement container snapshots and config backups&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;&lt;br&gt;
A Ryzen-powered beast in a mini PC shell. Dual 2.5 GbE, 10 GbE option, triple NVMe. Small box, big Proxmox energy.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Why I Ditched My VM NAS and Went Bare-Metal (And You Should Too)</title><link>https://diymediaserver.com/post/why_i_ditched_my_vm_nas_and_went_bare-metal/</link><pubDate>Thu, 24 Jul 2025 06:44:27 -0600</pubDate><guid>https://diymediaserver.com/post/why_i_ditched_my_vm_nas_and_went_bare-metal/</guid><description>&lt;img src="https://diymediaserver.com/post/why_i_ditched_my_vm_nas_and_went_bare-metal/featured_hu_f642412c99a856ef.webp" alt="Featured image of post Why I Ditched My VM NAS and Went Bare-Metal (And You Should Too)" /&gt;&lt;p&gt;My Jellyfin server used to forget movies. Random ones wouldn&amp;rsquo;t show up, or new ones would vanish into the ether. Reboot Proxmox, and poof, they&amp;rsquo;re back. The culprit? A race condition nightmare from running my NAS in a Proxmox-hosted VM.&lt;/p&gt;
&lt;p&gt;I tried everything. Automount. Systemd ordering. UID hacks. Ritual sacrifices to the filesystem gods. The best I ever got was &amp;ldquo;mostly works.&amp;rdquo; That&amp;rsquo;s not good enough when Jellyfin is your nightly unwind ritual.&lt;/p&gt;
&lt;p&gt;So I ditched the VM, went bare-metal with XFS and MergerFS, and finally built a NAS that boots clean and mounts right. Every. Single. Time.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Running your NAS inside a VM? Fine until you&amp;rsquo;re fighting race conditions you didn&amp;rsquo;t sign up for. Go bare-metal with XFS and MergerFS for simple, fast, rock-solid storage. You&amp;rsquo;re trading Proxmox&amp;rsquo;s creature comforts for predictable behavior that holds up.
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Define 7 XL can accommodate up to 18 HDDs/SSDs plus five additional SSDs in the Storage Layout, with flexible configurations using included multi-brackets and HDD/SSD trays.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A case like the Define 7 XL is what makes a bare-metal NAS practical. You need somewhere to physically put all those drives, and a chassis with proper drive trays beats stacking SSDs on a tower mid-plate any day. Now, why the VM approach falls apart in the first place.&lt;/p&gt;
&lt;h2 id="why-vm-nas-setups-sound-sexy-but-arent"&gt;Why VM NAS Setups Sound Sexy (But Aren&amp;rsquo;t)
&lt;/h2&gt;&lt;p&gt;It looks efficient on paper. One box. Multiple VMs. Snapshots. Live migrations. The homelab porn writes itself. Reality hits a bit different:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Race conditions from hell&lt;/strong&gt;: Proxmox boots, VMs start spinning up, but your containers beat NFS to the punch. Result? Jellyfin loads with a library that looks like Swiss cheese.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mounting nightmares&lt;/strong&gt;: Getting the Proxmox host to mount NFS shares &lt;em&gt;after&lt;/em&gt; the NAS VM boots is like herding cats. I tried automounts and failed spectacularly. Switched to systemd mounts. Same story. Finally built UID-mapped folders to sidestep Proxmox&amp;rsquo;s &lt;code&gt;100000&lt;/code&gt; offset nonsense. Worked 97% of the time. &amp;ldquo;Mostly working&amp;rdquo; storage is like being &amp;ldquo;mostly pregnant.&amp;rdquo;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Death by a thousand cuts&lt;/strong&gt;: Every virtualization layer (Proxmox to QEMU to ext4/XFS to NFS to LXC) adds latency. You bleed throughput. You sacrifice reliability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The debugging tax&lt;/strong&gt;: When things break, and they will, you&amp;rsquo;re troubleshooting across multiple abstraction layers. Is it the VM? The host? The container? The mount? Good luck figuring that out Sunday afternoon when your family is rioting because they can&amp;rsquo;t watch their favorite movie or the latest episode of their current indulgence.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;There is not much difference in performance if any at all.&amp;rdquo;&lt;/em&gt; - Some Reddit user who clearly never spent a weekend troubleshooting missing folders.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Try saying that after your fourth reboot hoping your media will magically reappear.&lt;/p&gt;
&lt;h2 id="why-bare-metal-xfs--mergerfs-wins"&gt;Why Bare-Metal XFS + MergerFS Wins
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No more race conditions&lt;/strong&gt;: NFS shares mount early via proper systemd ordering. Containers see their media, first boot, every boot, forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Direct I/O that doesn&amp;rsquo;t suck&lt;/strong&gt;: XFS is battle-tested and fast. MergerFS pools drives together without the virtualization overhead tax. Your drives work at their actual speed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Predictable boots&lt;/strong&gt;: No more crossing your fingers hoping your storage VM came up in time. No more UID hacks. Clean systemd dependencies that do what they say on the tin.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bonus wins&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full 10GbE bandwidth (not strangled by virtio drivers)&lt;/li&gt;
&lt;li&gt;Simpler storage stack, fewer things to break&lt;/li&gt;
&lt;li&gt;Cleaner disaster recovery: rsync, backups, and mounts you understand&lt;/li&gt;
&lt;li&gt;Sleep peacefully knowing your storage isn&amp;rsquo;t playing startup roulette&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bare-metal-vs-vm-the-real-scorecard"&gt;Bare-Metal vs. VM: The Real Scorecard
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VM NAS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✔️ Snapshots&lt;br&gt;✔️ Easy Proxmox backups&lt;br&gt;✔️ Service consolidation&lt;br&gt;✔️ Looks good in /r/homelab&lt;/td&gt;
&lt;td&gt;❌ NFS race conditions&lt;br&gt;❌ I/O performance tax&lt;br&gt;❌ Complex UID/GID mapping&lt;br&gt;❌ Multi-layer debugging hell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bare-Metal NAS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✔️ Predictable boots&lt;br&gt;✔️ Zero virtualization overhead&lt;br&gt;✔️ Simple, direct mounts&lt;br&gt;✔️ Full hardware performance&lt;/td&gt;
&lt;td&gt;⚠️ Manual backup strategy&lt;br&gt;⚠️ Extra box to power &amp;amp; manage&lt;br&gt;⚠️ No VM convenience features&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Power tradeoff?&lt;/strong&gt; Absolutely. That&amp;rsquo;s why I went with a G3220. It sips power like a gentleman but handles the workload without breaking a sweat. The 10Gb NIC and HBA get to stretch their legs properly.&lt;/p&gt;
&lt;h2 id="build-blueprint-the-setup-i-run"&gt;Build Blueprint: The Setup I Run
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt; (Stuff I had lying around):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel G3220&lt;/li&gt;
&lt;li&gt;Gigabyte GA-Z87X-D3H&lt;/li&gt;
&lt;li&gt;16GB DDR3&lt;/li&gt;
&lt;li&gt;LSI 9300-8i HBA in IT mode&lt;/li&gt;
&lt;li&gt;10Gb NIC&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Software Stack&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debian Trixie&lt;/li&gt;
&lt;li&gt;XFS on each drive&lt;/li&gt;
&lt;li&gt;MergerFS for pooling&lt;/li&gt;
&lt;li&gt;NFS for rock-solid container access&lt;/li&gt;
&lt;li&gt;systemd for proper mount ordering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this combo works&lt;/strong&gt;: Dead simple architecture. Nothing fancy. It boots and works, every time.&lt;/p&gt;
&lt;h3 id="for-details-on-mergerfs-and-hbas"&gt;For Details on MergerFS and HBAs:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/" &gt;Master the Basics - MergerFS the Best Way to Combine Drives for Your Home Media Server&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/hardware-why-you-need-an-hba/" &gt;Hardware - Why You Need an HBA&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The HBA is the part most people skimp on. If you&amp;rsquo;re running more than a handful of drives, an LSI card flashed to IT mode is the right answer. Onboard SATA falls apart once you push past six drives.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CFRKNJXL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9300-8i_hu_d34514ef05caf068.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9300-8i_hu_86413786deef51f7.webp" width="600" height="449" alt="LSI 9300-8i IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;LSI 9300-8i&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Already Flashed to IT mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3R5XJU0" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-cold-hard-truth-about-my-experience"&gt;The Cold, Hard Truth About My Experience
&lt;/h2&gt;&lt;p&gt;I burned two entire weekends trying to make VM-NAS race conditions disappear. They laughed at my attempts.&lt;/p&gt;
&lt;p&gt;I deployed automounts like a hopeful fool. I crafted systemd units with the precision of a Swiss watchmaker. I even UID-hacked workarounds that would make a kernel developer weep. And still, some folders only materialized after rebooting the entire Proxmox host.&lt;/p&gt;
&lt;p&gt;Bare-metal isn&amp;rsquo;t perfect. You lose snapshots. You lose centralized VM backups. You lose the satisfaction of running everything on one box.&lt;/p&gt;
&lt;p&gt;You gain something precious in return: &lt;strong&gt;predictability&lt;/strong&gt;. When you power on your NAS, it works. When containers start, they see their media. When users browse your library, the files are there.&lt;/p&gt;
&lt;p&gt;One box, one job, one stack to debug. That clarity is worth the extra 20 watts, especially when your CPU barely registers on the power meter and your Jellyfin setup never misses a beat.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom Line
&lt;/h2&gt;&lt;p&gt;A VM NAS &lt;em&gt;can&lt;/em&gt; work if you enjoy weekend troubleshooting sessions and the thrill of uncertainty. A bare-metal NAS &lt;em&gt;does&lt;/em&gt; work, every single time, without drama or surprise downtime.&lt;/p&gt;
&lt;p&gt;Tired of startup order roulette, phantom mount points, and explaining to family why half the movie collection disappeared again? Go physical. Go simple. Go fast.&lt;/p&gt;
&lt;p&gt;Your sanity will thank you. Your users will thank you. Your Saturday mornings will thank you.&lt;/p&gt;
&lt;h2 id="ready-to-build-it-right"&gt;Ready to Build It Right?
&lt;/h2&gt;&lt;p&gt;Ditch the VM complexity. Build your NAS properly with bare-metal XFS and MergerFS. Stop trusting virtualization layers to mount your media collection in the correct order.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got &lt;code&gt;fstab&lt;/code&gt; configs, systemd unit files, and plenty of battle scars if you need guidance.&lt;/p&gt;
&lt;p&gt;Ask away.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Why Unprivileged LXC + NFS = Regret (Here’s How I Learned the Hard Way)</title><link>https://diymediaserver.com/post/unprivileged_lxc_regret/</link><pubDate>Tue, 22 Jul 2025 06:04:43 -0600</pubDate><guid>https://diymediaserver.com/post/unprivileged_lxc_regret/</guid><description>&lt;img src="https://diymediaserver.com/post/unprivileged_lxc_regret/featured_hu_446d05e9cda86bff.webp" alt="Featured image of post Why Unprivileged LXC + NFS = Regret (Here’s How I Learned the Hard Way)" /&gt;&lt;p&gt;I spun up some unprivileged LXCs on Proxmox (Jellyfin, Sonarr, Radarr, and Audiobookshelf). The plan was to keep it lightweight, clean, and have them all talk to my NAS over NFS.&lt;/p&gt;
&lt;p&gt;That plan lasted about a week.&lt;/p&gt;
&lt;p&gt;What followed was a parade of silent failures, missing files, and permission bugs that made me question reality. Unprivileged sounded safe. Turns out LXCs are too stripped down to work reliably if you need NFS access.&lt;/p&gt;
&lt;p&gt;Let me show you why unprivileged LXC is a trap for anything that needs NFS access, and how I crawled back to VMs running Docker. Humbled but functional.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Unprivileged LXC + NFS seems awesome and secure until root mapping, id shifts, and bind mount hell break your stack. Use privileged LXC or, better yet for NFS, a VM. I learned this the hard way with Proxmox.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-two-faces-of-lxc-privileged-vs-unprivileged"&gt;The Two Faces of LXC: Privileged vs Unprivileged
&lt;/h2&gt;&lt;p&gt;LXC containers come in two flavors: &lt;strong&gt;privileged&lt;/strong&gt; and &lt;strong&gt;unprivileged&lt;/strong&gt;. That one word changes everything about how your containers behave, what they can access, and how much pain you&amp;rsquo;ll experience.&lt;/p&gt;
&lt;h3 id="privileged-containers-power-at-a-price"&gt;Privileged Containers: Power at a Price
&lt;/h3&gt;&lt;p&gt;A &lt;strong&gt;privileged container&lt;/strong&gt; runs as root on the host. Not “container root.” Actual host root.&lt;/p&gt;
&lt;p&gt;What that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container UID 0 = host UID 0&lt;/li&gt;
&lt;li&gt;Full access to system calls&lt;/li&gt;
&lt;li&gt;Direct read/write access to host files&lt;/li&gt;
&lt;li&gt;Minimal UID translation (things &lt;em&gt;work&lt;/em&gt; with NFS, USB devices, bind mounts, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sounds scary? It should be. If a service inside that container gets compromised, the attacker now has &lt;strong&gt;root on your host&lt;/strong&gt;. Full stop.&lt;/p&gt;
&lt;p&gt;When privileged LXC makes sense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal-only services you fully trust&lt;/li&gt;
&lt;li&gt;Containers behind a firewall, with zero external exposure&lt;/li&gt;
&lt;li&gt;Services that &lt;strong&gt;need&lt;/strong&gt; real root access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="unprivileged-containers-safe-but-issues-with-nfs"&gt;Unprivileged Containers: Safe but Issues with NFS
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Unprivileged containers&lt;/strong&gt; were designed to fix that security risk by adding a layer of UID mapping.&lt;/p&gt;
&lt;p&gt;They add 100000 to all container UIDs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container UID 0 → host UID 100000&lt;/li&gt;
&lt;li&gt;Container UID 1000 → host UID 101000&lt;/li&gt;
&lt;li&gt;And so on&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So even if something breaks inside, the container can&amp;rsquo;t mess with your host. It&amp;rsquo;s sandboxed. Safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But here’s the catch&lt;/strong&gt;: that UID mapping breaks everything outside the container.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NFS doesn’t recognize 100000 as root.&lt;/li&gt;
&lt;li&gt;File permissions stop making sense.&lt;/li&gt;
&lt;li&gt;Accessing anything shared with the host becomes a pain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unprivileged containers work great when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You don’t need to write to NFS&lt;/li&gt;
&lt;li&gt;You don’t care about permissions on shared files&lt;/li&gt;
&lt;li&gt;You want to sandbox a sketchy app or test environment&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="so-which-one-should-you-use"&gt;So Which One Should You Use?
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Best Container Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running Jellyfin/Arr stack with NFS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Virtual Machine&lt;/strong&gt; or Privileged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public-facing container&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Virtual Machine&lt;/strong&gt; or Unprivileged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker inside LXC&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Privileged&lt;/strong&gt; (not recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal utility that touches host fs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Privileged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe app or 3rd-party binary&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unprivileged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt;
Privileged = easier, more powerful, more dangerous
Unprivileged = safer, but crippled when it comes to real-world file access&lt;/p&gt;
&lt;p&gt;And if you’re mixing in NFS?
Unprivileged goes from “safe” to “useless.”&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
A Ryzen-powered beast in a mini PC shell. Dual 2.5 GbE, 10 GbE option, triple NVMe. Small box, big Proxmox energy.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="my-descent-into-troubleshooting-hell"&gt;My Descent Into Troubleshooting Hell
&lt;/h2&gt;&lt;p&gt;Let me walk you through the steps I took. Each one with hope, each one crushed by reality. If this looks like your future, do yourself a favor and skip ahead to VMs now.&lt;/p&gt;
&lt;h3 id="attempt-1-custom-uid-mapping"&gt;Attempt #1: Custom UID Mapping
&lt;/h3&gt;&lt;p&gt;I tried overriding &lt;code&gt;lxc.idmap&lt;/code&gt; to fake container UID 0 as host UID 1000:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;101000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1000&lt;/span&gt; &lt;span class="m"&gt;101000&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: u &lt;span class="m"&gt;1001&lt;/span&gt; &lt;span class="m"&gt;101001&lt;/span&gt; &lt;span class="m"&gt;64534&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lxc.idmap: g &lt;span class="m"&gt;1002&lt;/span&gt; &lt;span class="m"&gt;101002&lt;/span&gt; &lt;span class="m"&gt;64534&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Partially worked, but created more permission edge cases.&lt;/p&gt;
&lt;h3 id="attempt-2-nfs-export-tweaking"&gt;Attempt #2: NFS Export Tweaking
&lt;/h3&gt;&lt;p&gt;I tried various NFS settings and settled on this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/Storage/ *&lt;span class="o"&gt;(&lt;/span&gt;all_squash,anongid&lt;span class="o"&gt;=&lt;/span&gt;1001,anonuid&lt;span class="o"&gt;=&lt;/span&gt;1000,insecure,rw,fsid&lt;span class="o"&gt;=&lt;/span&gt;100,subtree_check&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Files appeared but the permissions were still an issue.&lt;/p&gt;
&lt;h3 id="attempt-3-host-mount--bind-mount"&gt;Attempt #3: Host Mount + Bind Mount
&lt;/h3&gt;&lt;p&gt;I mounted NFS on the host, then bind-mounted into the LXC:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp0: /mnt/storage/Shows,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/Shows
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp1: /mnt/storage/Movies,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/Movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp2: /mnt/storage/Music,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/Music
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp3: /mnt/storage/eBooks,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/eBooks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mp4: /mnt/storage/AudioBooks,mp&lt;span class="o"&gt;=&lt;/span&gt;/media/AudioBooks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Files showed up but were owned by &lt;code&gt;nobody:nogroup&lt;/code&gt;. Jellyfin couldn&amp;rsquo;t scan, play, or write. When the host mount failed during reboots, containers booted with empty folders. Silent disasters everywhere.&lt;/p&gt;
&lt;h3 id="attempt-4-the-bindfs-hail-mary"&gt;Attempt #4: The bindfs Hail Mary
&lt;/h3&gt;&lt;p&gt;In my final attempt, I tried &lt;code&gt;bindfs&lt;/code&gt; to remap ownership:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Unit&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Bindfs mounts &lt;span class="k"&gt;for&lt;/span&gt; NAS directories
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Requires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mnt-nas_storage.mount
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Service&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;oneshot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/bindfs -u &lt;span class="m"&gt;101000&lt;/span&gt; -g &lt;span class="m"&gt;101001&lt;/span&gt; /mnt/nas_storage /mnt/mapped_nas_storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Install&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: This worked the best, but I&amp;rsquo;d still randomly lose files. Every workaround fixed &lt;em&gt;one&lt;/em&gt; thing and broke &lt;em&gt;three more&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure someone has &lt;strong&gt;forced&lt;/strong&gt; unprivileged LXCs to work. I want to enjoy my media, not fight with permissions every weekend.&lt;/p&gt;
&lt;h2 id="the-privileged-container-security-trap"&gt;The Privileged Container Security Trap
&lt;/h2&gt;&lt;p&gt;So what&amp;rsquo;s the catch with using a privileged container to fix NFS? You&amp;rsquo;ve now handed root access from the container &lt;em&gt;directly&lt;/em&gt; to the host.&lt;/p&gt;
&lt;h3 id="root-inside--root-outside"&gt;Root inside = root outside
&lt;/h3&gt;&lt;p&gt;If someone compromises Jellyfin (or any dependency), they&amp;rsquo;re root on your Proxmox node. Game over.&lt;/p&gt;
&lt;h3 id="security-misconfigurations"&gt;Security Misconfigurations
&lt;/h3&gt;&lt;p&gt;Most people don&amp;rsquo;t harden their containers. Leave one small crack that someone could exploit, and the container can damage your host or pivot to other network devices.&lt;/p&gt;
&lt;h3 id="network-exposure-risks"&gt;Network Exposure Risks
&lt;/h3&gt;&lt;p&gt;Privileged containers can mess with host-level network settings. Bridge them carelessly, and an attacker could sniff or spoof traffic across your entire LAN.&lt;/p&gt;
&lt;h3 id="shared-kernel--shared-fate"&gt;Shared kernel = Shared fate
&lt;/h3&gt;&lt;p&gt;Containers don&amp;rsquo;t isolate the kernel. A bug in &lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;libva&lt;/code&gt;, or any media processing library could expose your entire host system.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a sandbox. It&amp;rsquo;s a potential backdoor.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D461YN33"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/SG3210X-M2_hu_933dcc0d5cd85f33.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/SG3210X-M2_hu_d7bfa040668d5558.webp" width="600" height="150" alt="TP-Link Omada SG3210X-M2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link Omada SG3210X-M2&lt;/strong&gt;
Full-featured, compact, rack-ready. Eight multi-gig ports, dual 10 GbE uplinks, VLAN/QoS/ACL/LACP, and seamless integration with TP‑Link’s Omada controller. It locks down your Jellyfin/NAS traffic while scaling effortlessly with your homelab.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/44ZP0Jk" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-solution-embrace-vms-with-docker"&gt;The Solution: Embrace VMs with Docker
&lt;/h2&gt;&lt;p&gt;After weeks of fighting with LXC permissions, I gave up and moved everything to VMs running Docker. Here&amp;rsquo;s why this works:&lt;/p&gt;
&lt;h3 id="true-isolation"&gt;True Isolation
&lt;/h3&gt;&lt;p&gt;VMs provide actual isolation. If Jellyfin gets compromised, the attacker is trapped in a virtual machine and not loose on your host.&lt;/p&gt;
&lt;h3 id="docker-handles-permissions"&gt;Docker Handles Permissions
&lt;/h3&gt;&lt;p&gt;Docker&amp;rsquo;s built-in user mapping and volume mounts handle NFS permissions much more gracefully than LXC&amp;rsquo;s UID mapping.&lt;/p&gt;
&lt;h3 id="predictable-behavior"&gt;Predictable Behavior
&lt;/h3&gt;&lt;p&gt;No more silent failures, missing files, or permission mysteries. Things work the way you expect them to.&lt;/p&gt;
&lt;h2 id="final-recommendations"&gt;Final Recommendations
&lt;/h2&gt;&lt;h3 id="for-jellyfin--nfs--remote-access"&gt;For Jellyfin + NFS + Remote Access:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Use a VM with Docker (best option)&lt;/li&gt;
&lt;li&gt;Use privileged LXC only if it&amp;rsquo;s internal-only and you understand the risks&lt;/li&gt;
&lt;li&gt;Avoid unprivileged LXC entirely for this use case&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="for-other-use-cases"&gt;For Other Use Cases:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Internal utilities: Privileged LXC is fine&lt;/li&gt;
&lt;li&gt;Public-facing services: Unprivileged LXC or VM&lt;/li&gt;
&lt;li&gt;Anything touching NFS: VM with Docker&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-bottom-line"&gt;&lt;strong&gt;The Bottom Line:&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Unprivileged LXC sounds safe until it isn&amp;rsquo;t. Add NFS, media servers, and remote access, and you&amp;rsquo;re stacking pain on top of pain.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve patched, mounted, and prayed my way through this mess. My final verdict? If you&amp;rsquo;re doing anything serious with NFS or Jellyfin, ditch the LXC complexity and use VMs with Docker.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t fight your stack. If you need NFS, go VM. If you&amp;rsquo;re exposing Jellyfin publicly, go VM. Save your sanity and your weekends.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BG685PKM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/TX201_hu_10ca205dbe5c76be.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/TX201_hu_2fc6531ff0fa03b3.webp" width="600" height="565" alt="TP-Link 2.5GB PCIe Network Card (TX201)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;TP-Link 2.5GB PCIe Network Card (TX201)&lt;/strong&gt;
Plug-and-play 2.5 GbE PCIe card that unlocks multi-gig speeds for about $30. Works out of the box with Proxmox, Linux, and Windows. No drama, only faster transfers.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4mafK0R" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Two Years of LXC Hell - Why I Crawled Back to Docker (And You Should Too)</title><link>https://diymediaserver.com/post/two-years-of-lxc-hell/</link><pubDate>Sun, 06 Jul 2025 07:29:44 -0600</pubDate><guid>https://diymediaserver.com/post/two-years-of-lxc-hell/</guid><description>&lt;img src="https://diymediaserver.com/post/two-years-of-lxc-hell/featured_hu_78422bffada785e6.webp" alt="Featured image of post Two Years of LXC Hell - Why I Crawled Back to Docker (And You Should Too)" /&gt;&lt;p&gt;So, you&amp;rsquo;re feeling clever. You&amp;rsquo;ve read the blogs, watched the YouTube tutorials, and decided that unprivileged LXC containers are the &amp;ldquo;right&amp;rdquo; way to run your Arr stack. Lightweight! Efficient! &lt;em&gt;So much better than Docker!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fast forward two years. You&amp;rsquo;re debugging NFS stale handle errors for the third night running, your downloads corrupt mid-transfer, and you&amp;rsquo;re questioning every life choice that led you to this moment.&lt;/p&gt;
&lt;p&gt;That was me. Now I&amp;rsquo;m back on Docker, tail between my legs, with a rock-solid &lt;code&gt;docker-compose.yml&lt;/code&gt; that works.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Thought LXC was smarter than Docker? So did I. Until stale NFS handles, ghost downloads, and permission nightmares broke my will to live. This Docker Compose setup works. Copy, run, exhale.
&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CLTNC6V6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="ASRock Mini-Desktop Computer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Need a Mini Server?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The DeskMini B760 is a compact and powerful barebone system perfect for homelab use. It supports 14th Gen Intel CPUs, dual DDR4 RAM up to 64GB, and fast storage via M.2 slots plus dual 2.5&amp;quot; drive bays. It&amp;rsquo;s a solid host for VMs or containers without eating a full rack.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4kVe2jP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-hard-truth-about-lxc-vs-docker-compose"&gt;The Hard Truth About LXC vs Docker Compose
&lt;/h2&gt;&lt;p&gt;Let me save you the pain I went through. LXC &lt;em&gt;sounds&lt;/em&gt; perfect for media servers until you try to run something complex like the full Arr suite and NFS shares.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what Docker Compose gives you that LXC never could:&lt;/p&gt;
&lt;h3 id="one-file-to-rule-them-all"&gt;One File to Rule Them All
&lt;/h3&gt;&lt;p&gt;With LXC, adding a new service means spinning up another container, configuring networking, setting up mounts, and praying everything talks to each other. With Docker Compose: Edit a few lines, run &lt;code&gt;docker compose up -d&lt;/code&gt;, and you&amp;rsquo;re done.&lt;/p&gt;
&lt;p&gt;Want to restart everything?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Want to migrate to a new server? Copy two files. Your &lt;code&gt;.env&lt;/code&gt; and your &lt;code&gt;docker-compose.yml&lt;/code&gt;. You&amp;rsquo;re back online in minutes.&lt;/p&gt;
&lt;h3 id="no-more-permission-purgatory"&gt;No More Permission Purgatory
&lt;/h3&gt;&lt;p&gt;Ever watch a file download successfully but never move to your media folder? Or see Sonarr throw &amp;ldquo;access denied&amp;rdquo; errors with zero explanation? That&amp;rsquo;s LXC&amp;rsquo;s unprivileged user permissions playing games with your sanity.&lt;/p&gt;
&lt;p&gt;This is the one I could never resolve: the file is downloaded and moved to the NFS share but the LXC host never sees it in the mount point until the system is rebooted.&lt;/p&gt;
&lt;p&gt;This Docker setup uses &lt;code&gt;PUID&lt;/code&gt; and &lt;code&gt;PGID&lt;/code&gt; across every container, so they all behave like the same user on your host. No more mystery permission errors. No more &lt;code&gt;chmod 777&lt;/code&gt; voodoo dances.&lt;/p&gt;
&lt;h3 id="shared-downloads-that-work"&gt;Shared Downloads That Work
&lt;/h3&gt;&lt;p&gt;One &lt;code&gt;/downloads&lt;/code&gt; directory for everything. SABnzbd drops files there. Sonarr, Radarr, and their friends watch that same folder and move files cleanly. No bind mount spaghetti. No symbolic link nightmares. Clean, predictable file handling.&lt;/p&gt;
&lt;h2 id="what-finally-broke-me"&gt;What Finally Broke Me
&lt;/h2&gt;&lt;p&gt;After two years of LXC &amp;ldquo;optimizations,&amp;rdquo; these were the straws that broke the camel&amp;rsquo;s back:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NFS + LXC = Stale File Handle Roulette&lt;/strong&gt;&lt;br&gt;
Try debugging why your mounts randomly go read-only mid-download or why Jellyfin can&amp;rsquo;t see a file that&amp;rsquo;s clearly in the share until you restart NFS. I spent &lt;em&gt;months&lt;/em&gt; chasing these ghosts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every Container Needs Its Own IP (Maybe)&lt;/strong&gt;&lt;br&gt;
Unless you NAT everything (gross), LXC means manually assigning static IPs and keeping track of them. Docker Compose skips this entirely. All your apps talk over an internal bridge network.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Updates Are a Gamble&lt;/strong&gt;&lt;br&gt;
Something breaks in LXC? Hope you documented every tweak you made over the past six months. Docker? Blow it away and recreate it in seconds. Your config survives because it&amp;rsquo;s volume-mounted.&lt;/p&gt;
&lt;h2 id="the-stack-that-finally-worked"&gt;The Stack That Finally Worked
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the battle-tested &lt;code&gt;docker-compose.yml&lt;/code&gt; that ended my suffering:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## PROWLARR ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prowlarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/prowlarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${PROWLARR_PORT}:9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/prowlarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## SONARR ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sonarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sonarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SONARR_PORT}:8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sonarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Shows:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## RADARR ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/radarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${RADARR_PORT}:7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Lidarr ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;lidarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/lidarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lidarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${LIDARR_PORT}:8686&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/lidarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Music:/music&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Readarr1 eBooks ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readarr1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/readarr:develop&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;readarr1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${READARR1_PORT}:8787&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/readarr1:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/eBooks:/books&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Readarr2 ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;readarr2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/readarr:develop&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;readarr2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${READARR2_PORT}:8787&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/readarr2:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/AudioBooks:/books&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Bazarr ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bazarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/bazarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;bazarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${BAZARR_PORT}:6767&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/bazarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## SABnzbd ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sabnzbd:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;${PUID}:${PGID}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SABNZBD_PORT}:8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;UMASK=0007&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;## NETWORK ##&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="what-each-piece-does"&gt;What Each Piece Does
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;services:&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Each app gets its own container. Easier to debug, update, or nuke from orbit when things go sideways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;image:&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
I use &lt;code&gt;linuxserver.io&lt;/code&gt; images exclusively. They&amp;rsquo;re clean, well-documented, and there are no weird surprises hiding in &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;user: &amp;quot;${PUID}:${PGID}&amp;quot;&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
This is the magic that prevents permission hell. Every container runs as your host user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;env_file: .env&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Keeps sensitive info and paths out of the compose file. Makes the whole thing portable between servers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;volumes:&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/config&lt;/code&gt;: App settings and databases that persist forever&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/downloads&lt;/code&gt;: Shared workspace for all apps and SAB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media&lt;/code&gt;: Your precious media collection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;networks:&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Everything sits on &lt;code&gt;media_network&lt;/code&gt;. Internal traffic stays inside Docker. No extra IPs to manage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Containers come back after reboots, crashes, or when you&amp;rsquo;re mid-binge and don&amp;rsquo;t want to babysit anything.&lt;/p&gt;
&lt;h2 id="the-essential-env-file"&gt;The Essential .env File
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-env" data-lang="env"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main user ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main group ID:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RADARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SONARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SABNZBD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PROWLARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9696&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LIDARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8686&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;READARR1_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8787&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;READARR2_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8788&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BAZARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6767&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DOWNLOADS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/downloads
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MEDIA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;id&lt;/code&gt; to get your &lt;code&gt;PUID&lt;/code&gt; and &lt;code&gt;PGID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;TZ&lt;/code&gt; correctly or your downloads will happen at weird hours&lt;/li&gt;
&lt;li&gt;Use absolute paths everywhere. Don&amp;rsquo;t get cute with relative paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="getting-started-the-easy-way"&gt;Getting Started (The Easy Way)
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Drop both files in the same folder:&lt;/strong&gt; Edit the &lt;code&gt;.env&lt;/code&gt; to match your real paths and UID/GID.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fire it up:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Access your apps:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sonarr: &lt;code&gt;http://your-ip:8989&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Radarr: &lt;code&gt;http://your-ip:7878&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Prowlarr: &lt;code&gt;http://your-ip:9696&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(and so on&amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="real-world-scenarios-where-this-shines"&gt;Real-World Scenarios Where This Shines
&lt;/h2&gt;&lt;h3 id="running-jellyfin"&gt;Running Jellyfin?
&lt;/h3&gt;&lt;p&gt;This stack becomes your automated feeder system. Jellyfin handles playback, and the Arr apps handle acquisition. New episode downloads → automatically appear in Jellyfin. No more manual file moves. No more metadata headaches.&lt;/p&gt;
&lt;h3 id="tired-of-snaps-bs-on-ubuntu"&gt;Tired of Snap&amp;rsquo;s BS on Ubuntu?
&lt;/h3&gt;&lt;p&gt;Snap has a mind of its own. Sometimes Snap won&amp;rsquo;t update, force-updates when you don&amp;rsquo;t want it to, or your Docker CLI vanishes.&lt;/p&gt;
&lt;p&gt;This stack uses real Docker, on your terms, with predictable behavior. (Also why I moved everything back to Debian, but that&amp;rsquo;s another rant.)&lt;/p&gt;
&lt;h3 id="want-boring-updates-yes-please"&gt;Want Boring Updates? Yes please.
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Your config persists, your ports don&amp;rsquo;t change, and you&amp;rsquo;re back online in seconds with fresh code.&lt;/p&gt;
&lt;h2 id="the-one-gotcha-thatll-bite-you"&gt;The One Gotcha That&amp;rsquo;ll Bite You
&lt;/h2&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
&lt;p&gt;&lt;strong&gt;Mounts matter.&lt;/strong&gt; If your paths don&amp;rsquo;t match between host and container, nothing works.&lt;/p&gt;
&lt;p&gt;Double-check that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Host &lt;code&gt;/downloads&lt;/code&gt; maps to container &lt;code&gt;/downloads&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Same for &lt;code&gt;/media&lt;/code&gt; and &lt;code&gt;/config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; matches your actual user&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get this wrong and you&amp;rsquo;ll be back to debugging permission errors like it&amp;rsquo;s 2019.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom Line
&lt;/h2&gt;&lt;p&gt;You now have the complete stack. One YAML file. One &lt;code&gt;.env&lt;/code&gt;. All your media apps work together without fighting over ports, permissions, or your sanity.&lt;/p&gt;
&lt;p&gt;I fought for two years trying to outsmart this problem with clever LXC setups. Turns out the solution was to stop being clever and let Docker do the work.&lt;/p&gt;
&lt;p&gt;I hope my pain and suffering saves you some time and effort.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0D22JRHZB"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/ugreen-NAS_hu_9b2b8c726e38b8a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/ugreen-NAS_hu_7dd7553917d2893c.webp" width="600" height="353" alt="UGREEN NASync DXP4800 Plus 4-Bay Desktop NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Need A NAS?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;UGREEN NASync DXP4800 Plus, 4-Bay NAS with an Intel Pentium Gold 8505 5-core CPU, 8GB DDR5, a 128GB built-in SSD, 2x M.2 NVMe slots, and both 10GbE and 2.5GbE ports (Diskless). This is perfect if you don&amp;rsquo;t want to DIY your NAS.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TZdbTa" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Docker vs Mutiple LXCs for the Arr Suite</title><link>https://diymediaserver.com/post/docker-vs-lxcs/</link><pubDate>Sun, 22 Jun 2025 07:13:47 -0600</pubDate><guid>https://diymediaserver.com/post/docker-vs-lxcs/</guid><description>&lt;img src="https://diymediaserver.com/post/docker-vs-lxcs/docker-vs-lxc_hu_c5120666824be778.webp" alt="Featured image of post Docker vs Mutiple LXCs for the Arr Suite" /&gt;&lt;p&gt;I used to think running each Arr app in its own LXC was a smart move. Felt clean. Minimal. Efficient. But if you&amp;rsquo;ve ever tried to juggle Sonarr, Radarr, SABnzbd, and the rest across a handful of Proxmox containers, you know the cracks show up fast. Trust me. I&amp;rsquo;ve been there, and it&amp;rsquo;s not worth the pain.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re serious about your media setup, move the Arr stack into a Docker environment inside a single VM. Here&amp;rsquo;s why it matters. (Want the step-by-step build rather than the argument? Follow the &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/" &gt;complete arr stack Docker Compose guide&lt;/a&gt;&lt;/strong&gt;.)&lt;/p&gt;
&lt;h2 id="points-that-im-willing-to-concede"&gt;Points That I&amp;rsquo;m Willing to Concede
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You can run the Arr suite in its own unprivileged LXCs. I did it. It worked. I didn&amp;rsquo;t like managing it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I know some of you run Docker inside an LXC. That&amp;rsquo;s a special kind of headache I want nothing to do with, and it isn&amp;rsquo;t officially supported by Proxmox.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-were-working-with"&gt;What We&amp;rsquo;re Working With
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Proxmox&lt;/strong&gt; is a virtualization platform that runs multiple operating systems on a single physical box. Perfect for a homelab or media hoarders consolidating services onto one machine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LXC containers&lt;/strong&gt; are lightweight virtualized environments that share the host&amp;rsquo;s kernel. Think isolated spaces that use fewer resources than full virtual machines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Virtual machines (VMs)&lt;/strong&gt; are complete, isolated operating systems that share nothing with the host.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Arr suite&lt;/strong&gt; is Sonarr (TV automation), Radarr (movie automation), Lidarr (music automation), and SABnzbd (download client). They work together to find, download, and organize your media library automatically.&lt;/p&gt;
&lt;h2 id="lxc-containers-look-great-on-paper-but-they-trip-you-up-fast"&gt;LXC Containers Look Great on Paper, But They Trip You Up Fast
&lt;/h2&gt;&lt;p&gt;LXCs are lightweight. They share the Proxmox host&amp;rsquo;s kernel and don&amp;rsquo;t suck up much in the way of resources. Sounds ideal, right? Until you realize how tightly integrated the Arr apps actually are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Too Many Moving Parts&lt;/strong&gt;: Seven LXCs means seven operating systems, seven update schedules, seven IP addresses, and seven things that can break. Docker bundles everything together. Less patching. Fewer surprises.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Networking Gets Messy&lt;/strong&gt;: Sonarr and Radarr need to talk to Prowlarr and your downloader constantly. Getting that cross-talk working between LXCs means manually configuring IP addresses, firewall rules, and routing between containers. Here&amp;rsquo;s where Docker shines. It creates an internal network where services find each other by name automatically. Instead of remembering that Sonarr lives at &lt;code&gt;192.168.1.100:8989&lt;/code&gt;, you point it at &lt;code&gt;sonarr:8989&lt;/code&gt; and Docker handles the rest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Storage and Permissions Are a Nightmare&lt;/strong&gt;: When multiple LXCs try to access the same media folders, you&amp;rsquo;ll run into UID and GID issues. One container creates files another can&amp;rsquo;t read or modify. With Docker, you map your storage once to the VM, and all containers share the same user context. Permission headaches gone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unprivileged Containers Make Network Storage a Pain&lt;/strong&gt;: If you&amp;rsquo;re running unprivileged LXCs (which you should, for security), accessing NFS or SMB shares becomes a special kind of hell. Unprivileged containers map user IDs differently than the host, so your media files might show up as owned by &lt;code&gt;nobody&lt;/code&gt; or become completely inaccessible. You&amp;rsquo;ll burn hours tweaking UID maps, CIFS mount options, and NFS export settings to get basic file access working. Meanwhile, a VM with Docker mounts your network shares normally and passes them through to containers without the mapping headaches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Config Drift Is Real&lt;/strong&gt;: One LXC gets updated while another doesn&amp;rsquo;t. Suddenly an app fails and you&amp;rsquo;re hunting down why a version mismatch is breaking API calls between services. Docker Compose gives you a single source of truth: a YAML file that pins your entire stack to specific versions.&lt;/p&gt;
&lt;h2 id="docker-in-a-vm-the-sweet-spot-for-media-automation"&gt;Docker in a VM: The Sweet Spot for Media Automation
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where things get easier.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One VM to Rule Them All&lt;/strong&gt;: Instead of babysitting a small army of containers, you manage one Debian or Ubuntu VM. Update once. Patch once. Reboot once.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Compose = Sanity&lt;/strong&gt;: Everything lives in a single &lt;code&gt;docker-compose.yml&lt;/code&gt; file. It handles every Arr app you need (Sonarr, Radarr, Lidarr, SABnzbd, etc.), their connections, volumes, ports, environment variables, user IDs, and timezones, all centralized and clean. Spin up the stack with &lt;code&gt;docker compose up -d&lt;/code&gt;. Shut it down with &lt;code&gt;docker compose down&lt;/code&gt;. Pull updates with &lt;code&gt;docker compose pull&lt;/code&gt; without touching the rest of the system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;True Isolation&lt;/strong&gt;: VMs are fully fenced off from your Proxmox host. If something breaks, it won&amp;rsquo;t leak into your main system. Need to move the stack? Back up the VM and drop it in the new location.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Less Headache When Things Break&lt;/strong&gt;: One place to look for problems. Run &lt;code&gt;docker logs &amp;lt;container&amp;gt;&lt;/code&gt; and get answers fast. No jumping between LXCs hunting through logs scattered across the host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Better Community Support&lt;/strong&gt;: Most media server guides today assume you&amp;rsquo;re running Docker. That means more tutorials, more troubleshooting help, and fewer dead ends.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sensible Use of Resources&lt;/strong&gt;: A single VM with Docker uses slightly more memory than a pile of LXCs up front. Long term, it&amp;rsquo;s more stable, more predictable, and easier to maintain. The small resource overhead is worth the massive reduction in complexity.&lt;/p&gt;
&lt;h2 id="stop-fighting-start-watching"&gt;Stop Fighting. Start Watching.
&lt;/h2&gt;&lt;p&gt;LXCs are great for simple, fire-and-forget services that don&amp;rsquo;t need to talk to each other. The Arr suite is the opposite. These apps need to work together without friction, and they need consistency across your entire stack.&lt;/p&gt;
&lt;p&gt;Docker inside a VM gives you that consistency. You get the benefits of isolation and virtualization with the simplicity of containerized apps that find each other on a shared network without fuss.&lt;/p&gt;
&lt;p&gt;So skip the mess. Build your media automation stack inside a VM. Use Docker Compose. Spend less time debugging networking and permissions, and more time enjoying your perfectly organized media library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set it up once. Watch it work. Enjoy your media.&lt;/strong&gt;&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CLTNC6V6"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/minidesktop_hu_1c4084a4918a496e.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/minidesktop_hu_b96e82c2c0dfa5d0.webp" width="600" height="470" alt="ASRock Mini-Desktop Computer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;ASRock Mini-Desktop Computer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The DeskMini B760 is a compact but capable barebone system perfect for homelab use. It supports up to 14th Gen Intel CPUs (65W), dual DDR4 RAM up to 64GB, and fast storage via both Gen5 and Gen4 M.2 slots plus dual 2.5&amp;quot; drive bays. Ideal for running lightweight VMs and containers in a tiny footprint.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4kVe2jP" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Choosing the Right Linux Distro Debian vs Ubuntu</title><link>https://diymediaserver.com/post/choosing-the-right-linux-distro-debian-vs-ubuntu/</link><pubDate>Sun, 15 Jun 2025 12:47:22 -0600</pubDate><guid>https://diymediaserver.com/post/choosing-the-right-linux-distro-debian-vs-ubuntu/</guid><description>&lt;img src="https://diymediaserver.com/post/choosing-the-right-linux-distro-debian-vs-ubuntu/debian-vs-ubuntu_hu_3a0fe267120ce0fd.webp" alt="Featured image of post Choosing the Right Linux Distro Debian vs Ubuntu" /&gt;&lt;p&gt;So you want to build a server that stores and shares files across your home network. A digital filing cabinet that every laptop, phone, and tablet in the house can hit. That&amp;rsquo;s a NAS, Network Attached Storage.&lt;/p&gt;
&lt;p&gt;When you go to build one, you&amp;rsquo;ll end up staring at two Linux distros: Debian or Ubuntu. There are others. For most home labs, the decision comes down to these two. Both are Linux. Both are free. They behave very differently under load, and that&amp;rsquo;s where the choice matters. Here&amp;rsquo;s why Debian is the better pick for a file server.&lt;/p&gt;
&lt;h2 id="what-makes-debian-and-ubuntu-different"&gt;What Makes Debian and Ubuntu Different?
&lt;/h2&gt;&lt;p&gt;Both are Debian-based, so the tooling looks identical from the command line. The difference is how each one decides what goes into the release.&lt;/p&gt;
&lt;p&gt;Debian is a carefully organized library. Every package is tested, then tested again, then frozen. Nothing ships until the maintainers are sure it plays nice with everything else on the shelf.&lt;/p&gt;
&lt;p&gt;Ubuntu is a modern bookstore. New releases land on the shelves fast. Sometimes the binding holds. Sometimes a new title doesn&amp;rsquo;t get along with the others you already own.&lt;/p&gt;
&lt;p&gt;For a file server, you want the library.&lt;/p&gt;
&lt;h2 id="why-debian-wins-for-file-servers"&gt;Why Debian Wins for File Servers
&lt;/h2&gt;&lt;h3 id="rock-solid-stability"&gt;Rock-Solid Stability
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;re storing family photos, tax documents, and the music collection. You want a system that doesn&amp;rsquo;t surprise you on a Tuesday morning.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No surprise changes.&lt;/strong&gt; Once Debian 13 ships, the behavior of the installed packages stays put. Same Samba config, same NFS exports, same MergerFS pool, for years.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer regressions.&lt;/strong&gt; Debian&amp;rsquo;s freeze cycle catches a lot of the upstream bugs before they hit your disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable upgrades.&lt;/strong&gt; You won&amp;rsquo;t wake up to find that an unattended-upgrade quietly changed how &lt;code&gt;smb.conf&lt;/code&gt; parses a directive.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ubuntu updates more aggressively. Sounds great until your file server reboots into a kernel that doesn&amp;rsquo;t love your HBA.&lt;/p&gt;
&lt;h3 id="lean-by-default"&gt;Lean by Default
&lt;/h3&gt;&lt;p&gt;Install Debian without a desktop and you get exactly what you asked for. Nothing else. Pick &amp;ldquo;SSH server&amp;rdquo; and &amp;ldquo;standard system utilities&amp;rdquo; in &lt;code&gt;tasksel&lt;/code&gt; and you&amp;rsquo;re done.&lt;/p&gt;
&lt;p&gt;What that gets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster boot.&lt;/strong&gt; Fewer services means &lt;code&gt;systemd-analyze blame&lt;/code&gt; is a short list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower RAM idle.&lt;/strong&gt; More memory left over for the ZFS ARC, the page cache, or whatever else is actually doing work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller attack surface.&lt;/strong&gt; Every package you don&amp;rsquo;t install is one you don&amp;rsquo;t have to patch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ubuntu Server pulls in snapd, cloud-init, and a stack of other things you didn&amp;rsquo;t ask for. Useful in a cloud VM. Noise on a home NAS.&lt;/p&gt;
&lt;h3 id="software-that-behaves"&gt;Software That Behaves
&lt;/h3&gt;&lt;p&gt;A file server needs three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Samba&lt;/strong&gt; for Windows clients over SMB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NFS&lt;/strong&gt; (&lt;code&gt;nfs-kernel-server&lt;/code&gt;) for Linux and Mac clients&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MergerFS&lt;/strong&gt; for pooling drives without a hardware RAID&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Debian gives you well-aged versions of all three. A five-year-old car with the recalls already done, versus a brand-new model on the first production run. When the data on the drives matters, the boring version is the right version.&lt;/p&gt;
&lt;p&gt;Ubuntu ships newer point releases. New is not always better when &amp;ldquo;better&amp;rdquo; means &amp;ldquo;doesn&amp;rsquo;t lose your data.&amp;rdquo;&lt;/p&gt;
&lt;h3 id="easy-major-version-upgrades"&gt;Easy Major-Version Upgrades
&lt;/h3&gt;&lt;p&gt;Every few years you&amp;rsquo;ll bump the OS. With Debian, the path is well-trodden. Edit &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;, swap &lt;code&gt;bookworm&lt;/code&gt; for &lt;code&gt;trixie&lt;/code&gt;, then run &lt;code&gt;apt update &amp;amp;&amp;amp; apt full-upgrade&lt;/code&gt;. Read the release notes first. They&amp;rsquo;re short and honest about the gotchas.&lt;/p&gt;
&lt;p&gt;Ubuntu&amp;rsquo;s &lt;code&gt;do-release-upgrade&lt;/code&gt; works most of the time. The rest of the time it picks a fight with a PPA you forgot about, or a snap that refuses to migrate, and you&amp;rsquo;re in recovery mode with no working machine. Ask me how I know.&lt;/p&gt;
&lt;h3 id="no-canonical-in-the-drivers-seat"&gt;No Canonical in the Driver&amp;rsquo;s Seat
&lt;/h3&gt;&lt;p&gt;Debian is run by volunteers who care about the project. Ubuntu is owned by Canonical, and Canonical has shareholders. That changes which decisions get made.&lt;/p&gt;
&lt;p&gt;What you get with Debian:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No snap.&lt;/strong&gt; Debian uses &lt;code&gt;apt&lt;/code&gt; and standard &lt;code&gt;.deb&lt;/code&gt; packages. No mandatory store backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No telemetry surprises.&lt;/strong&gt; Decisions happen on a public mailing list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No &amp;ldquo;Pro&amp;rdquo; upsell at the MOTD.&lt;/strong&gt; Your terminal stays quiet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="long-term-support"&gt;Long-Term Support
&lt;/h3&gt;&lt;p&gt;Both projects offer multi-year support windows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 24.04 LTS:&lt;/strong&gt; 5 years of standard updates. Extended to 10 years if you sign up for Ubuntu Pro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debian 13:&lt;/strong&gt; Roughly 5 years through the LTS team. Free. No account required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a decade on Ubuntu, you&amp;rsquo;re paying Canonical or registering machines against their free tier limit. On Debian, you wait for the next stable and run &lt;code&gt;apt full-upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-about-debian-13s-release-date"&gt;What About Debian 13&amp;rsquo;s Release Date?
&lt;/h2&gt;&lt;p&gt;Debian 13 (&amp;ldquo;Trixie&amp;rdquo;) isn&amp;rsquo;t shipped yet. The current target is mid-2025. If you&amp;rsquo;re building a NAS today, install Debian 12 (&amp;ldquo;Bookworm&amp;rdquo;) and ride the upgrade train when Trixie drops. The in-place upgrade is one of Debian&amp;rsquo;s best-tested code paths.&lt;/p&gt;
&lt;h2 id="debian-feels-like-home"&gt;Debian Feels Like Home
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve spent the last two weeks migrating all my VMs and LXC containers back to Debian. Nothing was broken. Debian feels like home. Clean, predictable, mine. No corporate layer between me and the system. The system I want, and nothing more.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The Bottom Line
&lt;/h2&gt;&lt;p&gt;A home file server is the foundation under your family&amp;rsquo;s data. You want reliable. You want long-lasting. You want the kind of system you can ignore for three years and have it still running when you ssh back in.&lt;/p&gt;
&lt;p&gt;Ubuntu 24.04 is fine. It comes with extra moving parts and a few Canonical decisions that get in the way of a quiet NAS. When the goal is file sharing that doesn&amp;rsquo;t fight you, Debian wins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For a home NAS, pick Debian.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="need-a-new-hdd-to-keep-up-with-your-downloads"&gt;Need a new HDD to keep up with your downloads?
&lt;/h3&gt;&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Prowlarr Docker Compose Setup Guide (2026)</title><link>https://diymediaserver.com/post/how-to-install-prowlarr-in-docker/</link><pubDate>Sun, 25 May 2025 06:24:52 -0600</pubDate><guid>https://diymediaserver.com/post/how-to-install-prowlarr-in-docker/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to-install-prowlarr-in-docker/prowlarr_hu_674a8102a74e6f4.webp" alt="Featured image of post Prowlarr Docker Compose Setup Guide (2026)" /&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Run Prowlarr in Docker with one Compose service, point it at your existing media stack, and let it sync indexers and downloaders to Sonarr, Radarr, Lidarr, and Readarr through the API. Configure once, never again.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;OS&lt;/dt&gt;&lt;dd&gt;Debian 13 (Trixie)&lt;/dd&gt;&lt;dt&gt;Docker&lt;/dt&gt;&lt;dd&gt;29.6.0&lt;/dd&gt;&lt;dt&gt;Sonarr&lt;/dt&gt;&lt;dd&gt;4.0.17&lt;/dd&gt;&lt;dt&gt;Radarr&lt;/dt&gt;&lt;dd&gt;6.2.1&lt;/dd&gt;&lt;dt&gt;Prowlarr&lt;/dt&gt;&lt;dd&gt;2.4.0&lt;/dd&gt;&lt;dt&gt;SABnzbd&lt;/dt&gt;&lt;dd&gt;5.0.4&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2026-06-20&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;strong&gt;Default port:&lt;/strong&gt; Prowlarr runs on &lt;strong&gt;port 9696&lt;/strong&gt;. Access the web UI at &lt;code&gt;http://your-server-ip:9696&lt;/code&gt; once the container is running.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I run this exact Compose stack on a Debian 12 box with the LinuxServer.io images, alongside Sonarr, Radarr, and SABnzbd on the same &lt;code&gt;media_network&lt;/code&gt; bridge. The config below is running on my box right now, copied straight out of the live compose file.&lt;/p&gt;
&lt;h2 id="what-is-prowlarr"&gt;What is Prowlarr?
&lt;/h2&gt;&lt;p&gt;Prowlarr is the index and download manager for your media server. It&amp;rsquo;s the glue between your torrent and Usenet sources and the Arr apps (Sonarr, Radarr, Lidarr, Readarr). Set up your indexers and downloaders once in Prowlarr, and it pushes them out to every Arr app over the API.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what it does day to day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connects to public and private torrent trackers, plus Usenet indexers.&lt;/li&gt;
&lt;li&gt;Wires your local downloaders (qBittorrent, SABnzbd, NZBGet) into the Arr apps.&lt;/li&gt;
&lt;li&gt;Centralizes every connection and monitors them for availability.&lt;/li&gt;
&lt;li&gt;Syncs indexers to your other Arr apps automatically.&lt;/li&gt;
&lt;li&gt;Categorizes indexers by media type (TV, movies, music) and sends them to the right app.&lt;/li&gt;
&lt;li&gt;Gives you one dashboard for every source your automation relies on. And when an indexer dies, Prowlarr tells you before Sonarr does.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-you-should-use-prowlarr"&gt;Why You Should Use Prowlarr
&lt;/h2&gt;&lt;p&gt;Running a media server without Prowlarr means logging into Sonarr, then Radarr, then Lidarr, pasting the same API key into each one, then doing it all again when an indexer changes its URL. Fine for a weekend. Painful by month three.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Saves time.&lt;/strong&gt; Add an indexer or downloader once. No more copy-pasting API keys and login info across three or four apps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduces errors.&lt;/strong&gt; When one app works and another doesn&amp;rsquo;t, the culprit is usually a stale or misconfigured indexer. Prowlarr keeps them aligned.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitors health.&lt;/strong&gt; You find out an indexer went offline today instead of a week later, when you&amp;rsquo;re missing the new episode of your show.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supports everything.&lt;/strong&gt; Private trackers, paid Usenet, free public indexers. Prowlarr handles all of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One messy manual setup becomes one clean, scalable system.&lt;/p&gt;
&lt;h2 id="step-1-install-docker"&gt;Step 1: Install Docker
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ll need Docker installed on your server first.
Walkthrough: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/" &gt;Master the Basics - How to Install Docker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-2-create-or-modify-your-docker-compose-file"&gt;Step 2: Create or Modify Your Docker Compose File
&lt;/h2&gt;&lt;p&gt;Define the Prowlarr container in &lt;code&gt;docker-compose.yml&lt;/code&gt;. The file below runs Prowlarr on its own. Already running the rest of the stack (say, from my &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;Sonarr guide&lt;/a&gt;&lt;/strong&gt;)? Drop the &lt;code&gt;prowlarr:&lt;/code&gt; service into your existing &lt;code&gt;services:&lt;/code&gt; block and reuse the same &lt;code&gt;media_network&lt;/code&gt; instead of pasting the whole thing.&lt;/p&gt;
&lt;h3 id="open-the-compose-file"&gt;Open the Compose File
&lt;/h3&gt;&lt;p&gt;Open your existing &lt;code&gt;docker-compose.yml&lt;/code&gt; or create a new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/docker-compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# PROWLARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prowlarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Official LinuxServer.io Prowlarr image&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/prowlarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Friendly name for the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Pulls PUID, PGID, UMASK, TZ, ports, and paths from the .env file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Maps the host port to Prowlarr&amp;#39;s web UI (9696) inside the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${PROWLARR_PORT}:9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Prowlarr&amp;#39;s config and database (persists across restarts)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Prowlarr only manages indexers, so it needs no media or downloads mounts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/prowlarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Lets `docker ps` show &amp;#34;healthy&amp;#34; once the UI starts answering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:9696/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Joins the shared media network so it can reach Sonarr/Radarr by name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Restart automatically unless you stop it yourself&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# NETWORK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared network, created once with: docker network create media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tip: Spacing matters. YAML is picky. Two-space indents only. No tabs.&lt;/p&gt;
&lt;h2 id="step-3-customize-the-env-file"&gt;Step 3: Customize the .env File
&lt;/h2&gt;&lt;h3 id="open-the-env-file"&gt;Open the .env File
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The contents should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main user ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main group ID:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# File permission mask&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;UMASK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0007&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RADARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SONARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SABNZBD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PROWLARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9696&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BAZARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6767&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DOWNLOADS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/downloads
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MEDIA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-4-start-prowlarr"&gt;Step 4: Start Prowlarr
&lt;/h2&gt;&lt;p&gt;The Compose file attaches to an external network called &lt;code&gt;media_network&lt;/code&gt;, so create it once before you bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker network create media_network
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bring the stack online:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm the container is running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;prowlarr&lt;/code&gt; in the list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;604d2ed3850c lscr.io/linuxserver/prowlarr:latest &lt;span class="s2"&gt;&amp;#34;/init&amp;#34;&lt;/span&gt; &lt;span class="m"&gt;6&lt;/span&gt; seconds ago Up &lt;span class="m"&gt;5&lt;/span&gt; seconds &lt;span class="o"&gt;(&lt;/span&gt;healthy&lt;span class="o"&gt;)&lt;/span&gt; 0.0.0.0:9696-&amp;gt;9696/tcp, &lt;span class="o"&gt;[&lt;/span&gt;::&lt;span class="o"&gt;]&lt;/span&gt;:9696-&amp;gt;9696/tcp prowlarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the container exits a few seconds after &lt;code&gt;up -d&lt;/code&gt;, check the logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs prowlarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Permission errors on &lt;code&gt;/config&lt;/code&gt; are the usual culprit. Step 5 fixes that.&lt;/p&gt;
&lt;h2 id="step-5-fix-permissions-if-needed"&gt;Step 5: Fix Permissions (If Needed)
&lt;/h2&gt;&lt;p&gt;Permissions are the number-one snag with media containers. Make sure your user and the &lt;code&gt;media&lt;/code&gt; group own everything the containers touch:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /docker/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /docker/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /media/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /media/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Want the full background on Linux permissions and why &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; matter?
&lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/" &gt;Master the Basics - Linux Permissions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-6-access-and-set-up-prowlarr-in-your-browser"&gt;Step 6: Access and Set Up Prowlarr in Your Browser
&lt;/h2&gt;&lt;p&gt;Open a browser and go to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://your-server-ip:9696&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The setup wizard walks you through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Adding your Usenet and torrent indexers&lt;/li&gt;
&lt;li&gt;Linking your downloader (qBittorrent, SABnzbd, or NZBget) via API&lt;/li&gt;
&lt;li&gt;Linking Sonarr, Radarr, Lidarr via API&lt;/li&gt;
&lt;li&gt;Setting categories (movies to Radarr, TV to Sonarr, and so on)&lt;/li&gt;
&lt;li&gt;Turning on Sync so Prowlarr pushes indexers automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s wired up, Prowlarr becomes the single place to update every indexer in your stack.&lt;/p&gt;
&lt;h2 id="step-7-keep-your-docker-software-updated"&gt;Step 7: Keep Your Docker Software Updated
&lt;/h2&gt;&lt;p&gt;To update:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull &lt;span class="c1"&gt;# Fetches the latest image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down &lt;span class="c1"&gt;# Stops and removes the running container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d &lt;span class="c1"&gt;# Starts a fresh container with the new image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prowlarr comes back up on the latest image. No reconfig needed.&lt;/p&gt;
&lt;h2 id="in-closing"&gt;In Closing
&lt;/h2&gt;&lt;p&gt;Prowlarr plugs the biggest gap in the Arr automation chain. It keeps indexers and downloaders consistent, online, and in sync with the rest of your stack. Run it in Docker and stop touching it.&lt;/p&gt;
&lt;p&gt;Next move: pin the LinuxServer.io image to a known-good tag (e.g. &lt;code&gt;lscr.io/linuxserver/prowlarr:1.21&lt;/code&gt;) once you&amp;rsquo;re happy with the setup. &lt;code&gt;latest&lt;/code&gt; is convenient until the day it isn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Building the whole automation stack rather than one app at a time? The &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/" &gt;complete arr stack Docker Compose guide&lt;/a&gt;&lt;/strong&gt; wires Prowlarr, Radarr, Sonarr, and Bazarr together in a single file.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_c4d4ce52ea706f03.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_759bf8113c1c8e11.webp" width="100" height="100" alt="Build the complete arr stack with Docker Compose on Proxmox" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Running the whole Arr suite?&lt;/strong&gt; Stand up Prowlarr, Radarr, Sonarr, and Bazarr from one Docker Compose file, with a fast download drive and clean imports into your media pool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Build the full stack
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="need-a-new-hdd-to-keep-up-with-your-downloads"&gt;Need a new HDD to keep up with your downloads?
&lt;/h3&gt;&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How To Install SABnzbd in Docker</title><link>https://diymediaserver.com/post/how-to_install-sabnzbd-in-docker/</link><pubDate>Sat, 10 May 2025 05:04:12 -0600</pubDate><guid>https://diymediaserver.com/post/how-to_install-sabnzbd-in-docker/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to_install-sabnzbd-in-docker/sabnzbd_hu_189552d4381eefab.webp" alt="Featured image of post How To Install SABnzbd in Docker" /&gt;&lt;p&gt;If you&amp;rsquo;re getting into Usenet, you&amp;rsquo;ll run into &lt;strong&gt;SABnzbd&lt;/strong&gt; fast. Radarr and Sonarr decide &lt;em&gt;what&lt;/em&gt; to download and &lt;em&gt;when&lt;/em&gt;. SABnzbd handles the &lt;em&gt;how&lt;/em&gt;. It grabs NZB files from your indexer, repairs and unpacks them, then drops the finished files into your media library.&lt;/p&gt;
&lt;p&gt;And the best part? Once it&amp;rsquo;s configured, it hums quietly in the background and you stop thinking about it.&lt;/p&gt;
&lt;h2 id="why-sabnzbd-is-a-core-part-of-your-media-stack"&gt;Why SABnzbd Is a Core Part of Your Media Stack
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why SABnzbd still wins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free and open source.&lt;/strong&gt; No premium licenses or paywalls. Fully featured out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles everything automatically.&lt;/strong&gt; Repair, extract, and cleanup using built-in &lt;code&gt;par2&lt;/code&gt; and &lt;code&gt;unrar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom categories.&lt;/strong&gt; Route TV, movies, and music into separate folders so Sonarr, Radarr, and Lidarr each get their own pile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web interface.&lt;/strong&gt; Clean, responsive UI for managing and monitoring downloads from any device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API and script support.&lt;/strong&gt; Post-processing scripts plus first-class integration with the *arr stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re on Usenet, this is the download client you want.&lt;/p&gt;
&lt;h2 id="why-run-sabnzbd-in-docker"&gt;Why Run SABnzbd in Docker?
&lt;/h2&gt;&lt;p&gt;Docker keeps SABnzbd clean and portable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cleaner installs.&lt;/strong&gt; No Python dependencies or package conflicts on the host.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effortless updates.&lt;/strong&gt; Pull the latest image and recreate the container.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to move.&lt;/strong&gt; Switching servers? Copy the config folder and the compose file. Done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better separation.&lt;/strong&gt; Your download service stays sandboxed from the rest of the system.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-1-install-docker"&gt;Step 1: Install Docker
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ll need Docker on your server. If you don&amp;rsquo;t have it yet, follow this guide: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/" &gt;Master the Basics - How to Install Docker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-2-create-or-modify-your-docker-compose-file"&gt;Step 2: Create or Modify Your Docker Compose File
&lt;/h2&gt;&lt;p&gt;Time to add SABnzbd to your stack. The file below runs SABnzbd on its own. Already running Sonarr and Radarr in a Compose file (say, from my &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;Sonarr guide&lt;/a&gt;&lt;/strong&gt;)? Drop the &lt;code&gt;sabnzbd:&lt;/code&gt; service into your existing &lt;code&gt;services:&lt;/code&gt; block and reuse the same &lt;code&gt;media_network&lt;/code&gt; instead of pasting the whole thing.&lt;/p&gt;
&lt;h3 id="open-the-compose-file"&gt;Open the Compose File
&lt;/h3&gt;&lt;p&gt;Open your existing &lt;code&gt;docker-compose.yml&lt;/code&gt; or create a new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/docker-compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# SABNZBD&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Official LinuxServer.io SABnzbd image&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sabnzbd:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Friendly name for the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Pulls PUID, PGID, UMASK, TZ, ports, and paths from the .env file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Maps the host port to SABnzbd&amp;#39;s web UI (8080) inside the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SABNZBD_PORT}:8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# SABnzbd&amp;#39;s config (persists across restarts)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared download folder (must be the same path Sonarr and Radarr use)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Lets `docker ps` show &amp;#34;healthy&amp;#34; once the UI starts answering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Joins the shared media network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Restart automatically unless you stop it yourself&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# NETWORK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared network, created once with: docker network create media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;💡 Tip: Watch the indentation. YAML is picky. Two spaces per level, never a &lt;code&gt;tab&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="step-3-customize-the-env-file"&gt;Step 3: Customize the .env File
&lt;/h2&gt;&lt;h3 id="open-the-env-file"&gt;Open the .env File
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The content of the .env file should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main user ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main group ID:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# File permission mask&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;UMASK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0007&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RADARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SONARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SABNZBD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PROWLARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9696&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BAZARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6767&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DOWNLOADS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/downloads
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MEDIA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Update the paths to match your setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CONFIG_PATH=/docker&lt;/code&gt; is the root folder where Docker stores persistent config.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOWNLOADS_PATH=/downloads&lt;/code&gt; is where your download client writes both incomplete and finished files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sonarr, Radarr, and SABnzbd all need to see the same &lt;code&gt;/downloads&lt;/code&gt; path inside their containers. SABnzbd writes finished files there, and Sonarr and Radarr copy them into your library on import. As long as all three mount the download drive at the identical &lt;code&gt;/downloads&lt;/code&gt; path, the apps find what SABnzbd finished with no remote path mapping.&lt;/p&gt;
&lt;p&gt;Also update the timezone (&lt;code&gt;TZ&lt;/code&gt;) to match yours. The full list lives here: &lt;a class="link" href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" target="_blank" rel="noopener"
&gt;valid timezones (Wikipedia)&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="step-4-start-sabnzbd"&gt;Step 4: Start SABnzbd
&lt;/h2&gt;&lt;p&gt;The Compose file attaches to an external network called &lt;code&gt;media_network&lt;/code&gt;, so create it once before you bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker network create media_network
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bring the container online:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check it&amp;rsquo;s running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;sabnzbd&lt;/code&gt; in the list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;d152d14e3fe3 lscr.io/linuxserver/sabnzbd:latest &lt;span class="s2"&gt;&amp;#34;/init&amp;#34;&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt; seconds ago Up &lt;span class="m"&gt;11&lt;/span&gt; seconds &lt;span class="o"&gt;(&lt;/span&gt;healthy&lt;span class="o"&gt;)&lt;/span&gt; 0.0.0.0:8080-&amp;gt;8080/tcp, &lt;span class="o"&gt;[&lt;/span&gt;::&lt;span class="o"&gt;]&lt;/span&gt;:8080-&amp;gt;8080/tcp sabnzbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the container isn&amp;rsquo;t there, check what happened:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose logs sabnzbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-5-fix-permissions-if-needed"&gt;Step 5: Fix Permissions (If Needed)
&lt;/h2&gt;&lt;p&gt;Permissions trip up everyone the first time. The &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; inside the container have to actually own the host directories, or SABnzbd can&amp;rsquo;t write to them. Fix it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /docker/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /docker/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /media/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /media/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the full breakdown of why this works, read this: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/" &gt;Master the Basics - Linux Permissions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-6-set-up-sabnzbd-in-your-browser"&gt;Step 6: Set Up SABnzbd in Your Browser
&lt;/h2&gt;&lt;p&gt;Open a web browser and visit:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://your-server-ip:8080&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Follow the setup wizard to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add your Usenet provider&amp;rsquo;s server info (host, port, username, password, connections)&lt;/li&gt;
&lt;li&gt;Set up download folders&lt;/li&gt;
&lt;li&gt;Create categories like &lt;code&gt;tv&lt;/code&gt; and &lt;code&gt;movies&lt;/code&gt; so Sonarr and Radarr can route downloads&lt;/li&gt;
&lt;li&gt;Grab the API key from &lt;code&gt;Config &amp;gt; General&lt;/code&gt; for Sonarr and Radarr to talk back&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s done, SABnzbd is the download engine for the rest of the stack.&lt;/p&gt;
&lt;h2 id="step-7-keep-your-docker-software-updated"&gt;Step 7: Keep Your Docker Software Updated
&lt;/h2&gt;&lt;p&gt;To update:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull &lt;span class="c1"&gt;# Fetches the latest image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down &lt;span class="c1"&gt;# Stops and removes the running container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d &lt;span class="c1"&gt;# Starts a fresh container with the new image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SABnzbd comes back up on the latest image with all your config intact.&lt;/p&gt;
&lt;h2 id="in-closing"&gt;In Closing
&lt;/h2&gt;&lt;p&gt;SABnzbd is the workhorse behind Usenet downloads. Pair it with Sonarr and Radarr and you have a media stack that runs itself. Running it in Docker keeps the install clean, makes upgrades a one-liner, and lets you move the whole thing between servers without rebuilding.&lt;/p&gt;
&lt;p&gt;If something breaks after this, the two places to look first are container logs (&lt;code&gt;docker compose logs sabnzbd&lt;/code&gt;) and host directory permissions on &lt;code&gt;/docker&lt;/code&gt; and &lt;code&gt;/media&lt;/code&gt;. Nine times out of ten, that&amp;rsquo;s where the problem is.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_c4d4ce52ea706f03.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_759bf8113c1c8e11.webp" width="100" height="100" alt="Build the complete arr stack with Docker Compose on Proxmox" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Running the whole Arr suite?&lt;/strong&gt; Stand up Prowlarr, Radarr, Sonarr, and Bazarr from one Docker Compose file, with a fast download drive and clean imports into your media pool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Build the full stack
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Master the Basics - How to Use parted to Create Partitions Format Them and Add a Mount Point</title><link>https://diymediaserver.com/post/master-the-basics-how-to-use-parted-to-create-partitions/</link><pubDate>Fri, 09 May 2025 06:24:47 -0600</pubDate><guid>https://diymediaserver.com/post/master-the-basics-how-to-use-parted-to-create-partitions/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-how-to-use-parted-to-create-partitions/parted_hu_42125f62c86a0269.webp" alt="Featured image of post Master the Basics - How to Use parted to Create Partitions Format Them and Add a Mount Point" /&gt;&lt;p&gt;You bought a new multi-terabyte drive for your media server and you&amp;rsquo;re ready to slap it into service. Hold up. Before you run &lt;code&gt;fdisk&lt;/code&gt; like it&amp;rsquo;s 1999, you should know it&amp;rsquo;s no longer the right tool for the job. If your drive is over 2TB, you need GPT instead of MBR. &lt;code&gt;fdisk&lt;/code&gt; can&amp;rsquo;t create GPT partitions. So &lt;code&gt;parted&lt;/code&gt; is the way to go.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll walk you through how to use &lt;code&gt;parted&lt;/code&gt; to create partitions, format them, and set up automatic mounting with &lt;code&gt;fstab&lt;/code&gt;. I&amp;rsquo;ve run this exact sequence on every new drive I&amp;rsquo;ve added to my mergerFS pool.&lt;/p&gt;
&lt;h3 id="why-parted-over-fdisk"&gt;Why Parted Over Fdisk?
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;fdisk&lt;/code&gt; only works with MBR partition tables, which caps your partition sizes at 2TB. That&amp;rsquo;s a deal-breaker for modern drives. &lt;code&gt;parted&lt;/code&gt; works with both MBR and GPT, and it doesn&amp;rsquo;t care how big the drive is.&lt;/p&gt;
&lt;p&gt;It also has better support for scripting, alignment, and resizing. If you&amp;rsquo;re working with advanced setups or want to future-proof your system, &lt;code&gt;parted&lt;/code&gt; is the tool to learn.&lt;/p&gt;
&lt;h3 id="fdisk-vs-parted-feature-comparison"&gt;fdisk vs parted: Feature Comparison
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;code&gt;fdisk&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;parted&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partition Table Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MBR (Master Boot Record) only&lt;/td&gt;
&lt;td&gt;MBR and GPT (GUID Partition Table)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maximum Disk Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Up to 2TB&lt;/td&gt;
&lt;td&gt;Supports disks larger than 2TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partition Resizing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supports resizing and moving partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filesystem Creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (requires separate tools like &lt;code&gt;mkfs&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Yes (can create filesystems during partitioning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Interface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text-based, menu-driven&lt;/td&gt;
&lt;td&gt;Command-line and scriptable interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advanced Features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic partitioning tasks&lt;/td&gt;
&lt;td&gt;Advanced features like alignment and scripting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple setups with MBR partitioning&lt;/td&gt;
&lt;td&gt;Complex setups, large disks, GPT partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="when-to-use-each-tool"&gt;When to Use Each Tool
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;fdisk&lt;/code&gt;&lt;/strong&gt; for straightforward partitioning on disks smaller than 2TB using the MBR scheme. Fine for legacy systems and simple setups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;parted&lt;/code&gt;&lt;/strong&gt; for disks larger than 2TB, anything that needs GPT, or anything that needs resizing and scripting. This is the modern default.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="setting-up-your-new-drive-with-parted"&gt;Setting Up Your New Drive with Parted
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s how to prep that new drive with &lt;code&gt;parted&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Identify the Target Disk&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsblk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for the disk you installed. If it&amp;rsquo;s &lt;code&gt;/dev/sdb&lt;/code&gt; and shows no partitions, that&amp;rsquo;s the one.&lt;/p&gt;
&lt;p&gt;⚠️ Triple-check you&amp;rsquo;ve got the right disk before you do anything else. Wipe the wrong one and you&amp;rsquo;ll cry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Install and Launch Parted, Then Create a GPT Partition Table&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Install parted and xfsprogs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install parted xfsprogs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Start parted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo parted /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set the disk label to GPT. This is what you want for drives over 2TB or UEFI systems:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mklabel gpt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 3: Create a New Partition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Still inside parted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkpart primary xfs 0% 100%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Swap out &lt;code&gt;xfs&lt;/code&gt; for another filesystem label if you plan to use ext4 or btrfs later. This step doesn&amp;rsquo;t actually format the drive. It defines what it&amp;rsquo;s &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Now type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;quit
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo partprobe /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 4: Format the New Partition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkfs.xfs /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can use &lt;code&gt;ext4&lt;/code&gt; if you prefer. For media servers handling big files, XFS is fast and reliable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Create a Mount Point&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Say you&amp;rsquo;re adding a 5th disk to a mergerFS pool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/pool0/disk5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Step 6: Mount It for Testing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Test it before you make anything permanent:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /dev/sdb1 /mnt/pool0/disk5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check that it worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;/dev/sdb1&lt;/code&gt; mounted at &lt;code&gt;/mnt/pool0/disk5&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 7: Add It to fstab&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, grab the UUID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo blkid /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll get something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;UUID=&amp;#34;abc123-xyz789&amp;#34; TYPE=&amp;#34;xfs&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Copy the UUID.&lt;/p&gt;
&lt;p&gt;Edit your fstab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add this line:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;UUID=abc123-xyz789 /mnt/pool0/disk5 xfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Save and exit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 8: Test Your fstab Config&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Unmount the drive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/pool0/disk5
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now reload fstab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No errors? You&amp;rsquo;re good.&lt;/p&gt;
&lt;h3 id="wrapping-up"&gt;Wrapping Up
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;parted&lt;/code&gt; is the right tool when you&amp;rsquo;re working with modern storage. You&amp;rsquo;ve now set up your drive with a GPT table, created a partition, formatted it, and wired it into fstab. All without hitting the 2TB wall that haunts &lt;code&gt;fdisk&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next time you drop a new drive into your rig, reach for &lt;code&gt;parted&lt;/code&gt; first.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t have a new drive yet? Pick one up here:&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The Best and Worst PC Cases for Media Servers – Ranked and Reviewed</title><link>https://diymediaserver.com/post/review-best-desktop-cases-nas-build/</link><pubDate>Sat, 03 May 2025 07:39:35 -0600</pubDate><guid>https://diymediaserver.com/post/review-best-desktop-cases-nas-build/</guid><description>&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/desktop_case_hu_9e20ad305983ee68.webp" alt="Featured image of post The Best and Worst PC Cases for Media Servers – Ranked and Reviewed" /&gt;&lt;p&gt;You don’t need RGB LEDs and tempered glass side panels to build a good home server. You need airflow. You need drive bays. And most of all, you need peace of mind. Most PC cases aren’t built for this. They’re built for looks, not longevity. If you&amp;rsquo;re stacking hard drives and chasing silence, the case matters more than you think.&lt;/p&gt;
&lt;p&gt;I’ve built more home servers than I can count. Friends, clients, online strangers, all run into the same wall: case selection. They assume any big or “premium” tower will do. Then they try to cram 12 spinning drives into a gaming case and realize there’s no airflow, no cable space, and no way to keep it quiet. The whole thing sounds like an F/A-18 Super Hornet and cooks the hard drives like an air fryer.&lt;/p&gt;
&lt;p&gt;This is not the way.&lt;/p&gt;
&lt;p&gt;Home media servers aren’t about flexing GPU specs or running rainbow light shows. They’re about stacking storage, managing heat, and keeping your build dead silent. Whether you’re building a 100TB media monster or a modest 24TB file server, the case is the make-or-break stat.&lt;/p&gt;
&lt;p&gt;So here’s the shortlist. I’ve ranked the cases I’ve actually built in, based on hands-on time, airflow, cable management sanity, and raw drive capacity. Every pick includes specs and an honest verdict. No sponsorships. No hype. Only what works when you’re building for the long haul.&lt;/p&gt;
&lt;h2 id="-s-tier--best-of-the-best"&gt;🟩 S-Tier – Best of the Best
&lt;/h2&gt;&lt;h2 id="fractal-design-define-7-xl"&gt;&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.fractal-design.com/products/cases/define/define-7-xl/" target="_blank" rel="noopener"
&gt;Factal Design&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/45deg_hu_e509aad8ec009590.avif"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/45deg_hu_2859ab515bd28540.webp" width="198" height="250" alt="Fractal Design Define 7 XL 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side1_hu_3a6e880201030067.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side1_hu_3889bf2b78c77916.avif 528w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side1_hu_3ff587c5ade1214a.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side1_hu_3ff587c5ade1214a.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side1_hu_7bc6123f94ac2df9.webp 528w" width="300" height="284" alt="Fractal Design Define 7 XL Side1" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side2_hu_4fa414753ef4e7db.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side2_hu_585a6a58a4514d42.avif 524w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side2_hu_3b9b5573547df202.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side2_hu_3b9b5573547df202.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_7_XL/side2_hu_8a52df2c113b6457.webp 524w" width="300" height="286" alt="Fractal Design Define 7 XL Side2" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;The Define 7 XL is the endgame for serious NAS and media server builders. It’s big, heavy, and not cheap, but it earns every inch and dollar. With room for up to 18 drives, whisper-quiet acoustics, and a modular layout that adapts to your needs, it’s the only consumer case that handles a 100TB+ array without hacks or headaches.&lt;/p&gt;
&lt;p&gt;This is the gold standard. Built like a tank, cooled like a server, and quiet enough to live next to your desk. The Define 7 XL dominates long-term, high-capacity builds.&lt;/p&gt;
&lt;p&gt;Want to build it once and use it forever? This is the case you buy.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Full Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 23.8&amp;quot; × 9.4&amp;quot; × 22.3&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: E-ATX / ATX / mATX / mITX / EE-ATX / SSI-CEB / SSI-EEB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: You can get up to 18 3.5”/2.5” drives inside with optional trays.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Three 140mm fans out of the box. You can fit a 480/420mm rad in the top, another 480/420mm rad in the front, and still have room to breathe. Up to 11 120mm fans or 9 140mm fans can be used in this case. Airflow is good, even fully loaded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: You’ve got space behind the motherboard tray for thick SATA and power cables, even with 10+ drives. Velcro straps and deep channels make routing easy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: Sound-dampening panels on all major surfaces. It’s incredibly quiet, even with a full drive load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Thick steel, zero flex, excellent panel fitment. You can tell Fractal didn’t cheap out here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, plastic, sound-dampened panels&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: It’s big. You need room on or under your desk. And it’s not cheap. Worth every penny.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: If you&amp;rsquo;re serious about building a media server you can set and forget, the Define 7 XL is the one to beat. Yes, it&amp;rsquo;s expensive, but you&amp;rsquo;ll never outgrow it.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="silverstone-cs380b"&gt;&lt;strong&gt;SilverStone CS380B&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.silverstonetek.com/en/product/info/server-nas/CS380/" target="_blank" rel="noopener"
&gt;SilverStone&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg_hu_8d513fe4d0ff83ca.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg_hu_cff77095da4fe4ae.avif 567w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg_hu_2e80fcc43c6406f1.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg_hu_2e80fcc43c6406f1.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg_hu_9266972eb18f1298.webp 567w" width="300" height="265" alt="SilverStone CS380B 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg2_hu_4230cf3f2282e06a.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg2_hu_34b1dbe27584af5b.avif 492w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg2_hu_e25cd24d2e373dfa.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg2_hu_e25cd24d2e373dfa.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/45deg2_hu_b2c1c5cf8a8fcae2.webp 492w" width="300" height="305" alt="SilverStone CS380B Side1" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/side_hu_658cded02c5e1cc0.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/side_hu_417416c0be72a711.avif 568w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/side_hu_52f98bf1fd2dce44.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/side_hu_52f98bf1fd2dce44.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CS380B/side_hu_59a502b18781ffb5.webp 568w" width="300" height="264" alt="SilverStone CS380B Side2" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;Built for one job, and built well. The SilverStone CS380B is a no-nonsense NAS case with real hot-swap support at a price that won’t gut your budget. You get 8 hot-swap bays with proper backplanes, solid cooling, and a compact footprint perfect for closet installs or headless setups.&lt;/p&gt;
&lt;p&gt;Yeah, the internal layout is cramped. Cable routing is a pain. And no, it won’t win any beauty contests. But if you care more about function than flash, this case delivers what matters: easy drive access, reliable airflow, and a build that’s purpose-built for storage.&lt;/p&gt;
&lt;p&gt;If hot-swapping is a must, this is the case to get.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 16.8&amp;quot; × 8.5&amp;quot; × 19.2&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 8 hot-swap bays in the front, each with a metal tray and built-in backplane. Add two 5.25&amp;quot; bays above for accessories or more drives with adapters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Two 120mm side fans and a 120mm rear fan keep drives reasonably cool. Load all 8 bays and you’ll want better fans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: This is where it hurts. It’s tight, especially around the drive bay area. You’ll fight with SATA power cables. Modular PSUs help.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: Not dampened. The stock fans are loud. You’ll hear the drives spinning. Not ideal for a living room.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: The frame is solid, but the plastic front door feels cheap and wobbly. Hinges are known to pop loose over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: SECC steel, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: Limited GPU space (9.5”) and low CPU cooler clearance (5.7”) restrict future upgrades.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: It’s a bit awkward to build in, but for a dedicated NAS tucked in a closet or rack, this case nails the essentials: hot-swap support at a fair price.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: SilverStone has discontinued the CS380B, and it no longer shows stock on Amazon. The current equivalent is the &lt;strong&gt;CS382&lt;/strong&gt;, which keeps the eight hot-swap bays, moves them to SAS-12G/SATA-6G trays with a status LED per drive, adds USB-C to the front panel, and fits a 240 or 280mm radiator. One change matters before you swap one for the other: the CS382 tops out at Micro-ATX, so a full-size ATX board that fit the older case won’t fit this one. Everything above describes the CS380B.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CKTYSZV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/cs382_hu_c2d47a48a134d684.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/cs382_hu_20397b76262d306b.webp" width="600" height="698" alt="SilverStone CS382" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;SilverStone CS382&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The successor to the CS380B reviewed above. Same eight hot-swap bays, now Micro-ATX only.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B03ecS0QX" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="-a-tier--strong-performers-with-minor-quirks"&gt;🟨 A-Tier – Strong Performers with Minor Quirks
&lt;/h2&gt;&lt;h2 id="fractal-design-define-r5"&gt;&lt;strong&gt;Fractal Design Define R5&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.fractal-design.com/products/cases/define/define-r5/black/" target="_blank" rel="noopener"
&gt;Fractal Design&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/45deg_hu_1623f8f0a91a60b9.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/45deg_hu_34a6b1d6b8690e62.avif 452w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/45deg_hu_410205cd47dd61d5.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/45deg_hu_410205cd47dd61d5.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/45deg_hu_55ca639dcd98ad84.webp 452w" width="300" height="332" alt="Fractal Design Define R5 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/hdd_hu_8c09e68ab4576cf6.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/hdd_hu_d6cb4ee772b3d3bd.avif 497w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/hdd_hu_839ee33f1e4449f3.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/hdd_hu_839ee33f1e4449f3.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/hdd_hu_4b76489dabf54ed.webp 497w" width="300" height="302" alt="Fractal Design Define R5 HDD View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/side2_hu_cf2472bc3f43f4b7.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/side2_hu_347b329165e44a62.avif 562w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/side2_hu_21c32ad35213ad7d.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/side2_hu_21c32ad35213ad7d.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Define_R5/side2_hu_d6a4d485f7f446cb.webp 562w" width="300" height="267" alt="Fractal Design Define R5 Side2" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;The Define R5 is the quiet, compact workhorse of the Fractal lineup. Think of it as the little brother to the Define 7 XL. Smaller, easier to fit into media centers or home offices, and still packed with premium features. You lose some drive slots but gain acoustic dampening, modular cages, and a build that feels perfect.&lt;/p&gt;
&lt;p&gt;It’s not built for massive arrays or hot-swapping. But for up to 10 drives in a whisper-quiet setup, the R5 hits a sweet spot. Perfect for bedroom or office servers where silence matters more than scale.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 20.5&amp;quot; × 9.1&amp;quot; × 17.8&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Comes with 8 combo 3.5&amp;quot;/2.5&amp;quot; bays plus 2 SSD mounts. Enough for a modest media or backup server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Two quiet 140mm fans included. Add more if needed. This case supports up to 9 fans total or a 420mm rad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Generous space behind the board. It’s easy to keep things clean, even with a bunch of SATA runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: It’s quiet. One of the quietest mid-towers you can buy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Excellent. Panels are thick, the door is sturdy, and everything lines up perfectly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, plastic, sound-dampened panels&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: You’re capped at 10 total drives, and no hot-swap. Front I/O is aging. No USB-C.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: If you want a whisper-quiet server in the same room you work or sleep in, the R5 is a top choice. Keep in mind the front I/O is a little dated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Fractal has discontinued the Define R5, and Amazon now lists it as unavailable rather than out of stock. If you want the same sound-dampened build quality from a case you can still buy, the Define 7 XL reviewed at the top of this post is the current option. It&amp;rsquo;s a full tower rather than a mid tower, so it&amp;rsquo;s bigger and pricier than the R5 was, but it carries the acoustic panels and modular cages forward. Everything above describes the R5.&lt;/p&gt;
&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The current Fractal option now that the R5 is discontinued. Reviewed in full at the top of this post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="montech-king-95-pro"&gt;&lt;strong&gt;Montech KING 95 PRO&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.montechpc.com/king-95-pro" target="_blank" rel="noopener"
&gt;Montech&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/45deg_hu_1779a9a400e7f44f.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/45deg_hu_504894f0e8491265.avif 435w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/45deg_hu_ada44549740abe1c.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/45deg_hu_ada44549740abe1c.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/45deg_hu_8c9edadc39fd765.webp 435w" width="300" height="345" alt="Montech KING 95 PRO 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/side_hu_2f27fa084f08c36c.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/side_hu_af5e59f0aab93909.avif 600w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/side_hu_c4777603af90a77f.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/side_hu_c4777603af90a77f.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/side_hu_341dc0639f203c9f.webp 600w" width="300" height="300" alt="Montech KING 95 PRO Side" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/back_hu_23a97ad46624e428.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/back_hu_6ae6f9e01a86aae0.avif 345w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/back_hu_73d1d98564d09417.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/back_hu_73d1d98564d09417.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/King/back_hu_5738822895f9b1e7.webp 345w" width="300" height="435" alt="Montech KING 95 PRO Back" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;Want a media server that performs well and looks good doing it? The KING 95 PRO delivers. With space for up to 13 drives, strong airflow, and a dual-chamber layout, it has real substance behind the looks. The tempered glass and ARGB fans add flair without getting in the way.&lt;/p&gt;
&lt;p&gt;It’s not a perfect case, but for the price, it’s surprisingly capable. The budget-friendly case that proves your server doesn’t have to look like a file cabinet to get the job done.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 18.7&amp;quot; × 11.8&amp;quot; × 17.4&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 8 x 3.5&amp;quot; HDDs and 5 x 2.5&amp;quot; SSDs. Enough for most users unless you’re running a full-blown archive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Comes with six PWM ARGB fans and a 10-port hub. Excellent airflow. Great out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Dual-chamber layout makes routing easy. You’ll have plenty of room to hide cables.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: Not bad, but no insulation. Fan curve tuning will matter here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Surprisingly solid. Panels fit well. The curved tempered glass is sturdy, but it’s a fingerprint magnet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: 0.8mm SPCC steel, tempered glass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: No hot-swap. Bottom filter is a pain to clean. Riser cable for vertical GPU not included.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Slick, spacious, and stylish. This case looks great in an office or home theater setup. It’s not for massive arrays or hot-swapping, and build quality isn’t perfect, but for the price, it punches way above its weight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The KING 95 PRO is no longer available on Amazon, so I&amp;rsquo;ve pulled the buy link. The review stays up because the case still turns up secondhand, and it&amp;rsquo;s worth knowing what you&amp;rsquo;re getting if one comes your way. For a new build at this capacity, the S-tier picks at the top of this post are where I&amp;rsquo;d look now. Everything above describes the KING 95 PRO.&lt;/p&gt;
&lt;h2 id="rosewill-helium-nas"&gt;&lt;strong&gt;Rosewill Helium NAS&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.rosewill.com/p/9SIA072KE80547" target="_blank" rel="noopener"
&gt;Rosewill&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/45deg_hu_12cafe07a79ec43e.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/45deg_hu_7543393a29253324.avif 316w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/45deg_hu_5b8affff220db084.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/45deg_hu_5b8affff220db084.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/45deg_hu_52edc6be7e52588b.webp 316w" width="300" height="475" alt="Rosewill Helium NAS 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side_hu_c95b7d611d81e3a4.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side_hu_924294789e4297bf.avif 431w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side_hu_dbab7fdcb3d48168.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side_hu_dbab7fdcb3d48168.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side_hu_c79a3faad00a2fa8.webp 431w" width="300" height="348" alt="Rosewill Helium NAS Side" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side2_hu_a9864bc28f081b15.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side2_hu_753900319463ad4c.avif 436w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side2_hu_9c242decac857580.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side2_hu_9c242decac857580.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Helium/side2_hu_3fc784d57db3530b.webp 436w" width="300" height="344" alt="Rosewill Helium NAS Side 2" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;The Helium NAS is all about stuffing in drives without draining your wallet. It’s not polished. The case suffers from thin panels, sharp edges, and noisy stock fans. But for under $100, it gets the job done. You get high drive capacity, reasonable airflow, and enough room to build something serious on a tight budget.&lt;/p&gt;
&lt;p&gt;It’s rough around the edges. If you care more about storage than style, the Helium NAS delivers real value where it counts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 19.2&amp;quot; × 8.5&amp;quot; × 19.2&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 10 x 3.5&amp;quot; + 3 x 2.5&amp;quot;. Huge capacity for the price.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Four 140mm fans stock. All intake by default and needs reconfiguration for proper airflow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Tight. Especially with full SATA and Molex bundles. Plan your routing before plugging anything in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: Mesh front and aggressive airflow = louder than you&amp;rsquo;d expect. But temps stay good.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: It’s budget steel. Side panels flex. Drive cage fitment isn’t always great. Expect quirks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: SECC steel, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: No hot-swap. Some drives may block airflow. Front I/O is decent though, with USB-C included.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: You’ll fight some build quirks, but for under $100, it’s a solid budget case for stuffing full of drives. 13 bays and mesh airflow seal the deal.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DP3PQFKW"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/rosewill-helium_hu_5126aadb3fcc1746.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/rosewill-helium_hu_69bfd459d0bfd04c.webp" width="600" height="898" alt="Rosewill Helium NAS" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Rosewill Helium NAS&lt;/strong&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0g6YNIYR" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="-b-tier--usable-if-youre-handy"&gt;🟧 B-Tier – Usable if You&amp;rsquo;re Handy
&lt;/h2&gt;&lt;h2 id="darkrock-classico-max"&gt;&lt;strong&gt;DARKROCK Classico Max&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://darkrockpc.com/products/darkrock-classico-max-storage-master-case-e-atx-computer-case-with-4x120mm-black-fan-10-x3-5-hdd-3-x2-5-sdd-usb-3-0-ready-360mm-radiator-supported-vertical-slots-for-gpu-vertically-mount-black" target="_blank" rel="noopener"
&gt;DARKROCK&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/45deg_hu_bfb7fa206af898c3.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/45deg_hu_3ee680f341d65e89.avif 484w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/45deg_hu_d6f77493d140cfaf.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/45deg_hu_d6f77493d140cfaf.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/45deg_hu_aaacb3c07acf8a32.webp 484w" width="300" height="310" alt="DARKROCK Classico Max 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/explode_hu_71d1f80df0d882e9.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/explode_hu_71969cf0881e1917.avif 563w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/explode_hu_4bc335f0f0f6e858.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/explode_hu_4bc335f0f0f6e858.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/explode_hu_a51259073fbff6de.webp 563w" width="300" height="266" alt="DARKROCK Classico Max Exploded View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/side_hu_65e021707fe21869.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/side_hu_aa415e49dfeee2bb.avif 479w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/side_hu_40240a2205daa0e7.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/side_hu_40240a2205daa0e7.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/DarkRock/side_hu_757301ef17216158.webp 479w" width="300" height="313" alt="DARKROCK Classico Max Side" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;This case aims for maximum drive count on a budget and mostly nails it. It has space for 13 drives, four pre-installed fans, and a clean exterior, all for around $90. It’s a solid foundation for a media server, especially if you’re trying to stretch every dollar.&lt;/p&gt;
&lt;p&gt;The build quality won’t blow you away. Thin panels, tight cable routing, and sharp edges make the build process a hassle. But if you have patience and a first aid kit (you will cut your hands), it’s hard to beat the value for the drive count.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 18.9&amp;quot; × 8.3&amp;quot; × 18.5&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: E-ATX (with drive cage removed) / ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 10 x 3.5&amp;quot;, 3 x 2.5&amp;quot;. Impressive at this price.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Four fans included, and it fits a 360mm rad on top. Drive area airflow could be better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Weak spot. Rear cable space is limited. SATA and Molex cables bulge the panel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: No dampening. Expect noticeable fan and drive noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Metal panels are thin. Side panel fitment is hit-or-miss.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: USB-C missing. One USB 3.0 port only. Watch out for cable bulk behind the tray.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Need maximum drive count at a low cost? This case has you covered (in your own blood). Expect thin panels and a serious fight for cable space.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0DM8NMFZL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/darkrock-classico-max_hu_34adaf8a1b3548d4.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/darkrock-classico-max_hu_a4141e70f15f3561.webp" width="600" height="620" alt="DARKROCK Classico Max" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;DARKROCK Classico Max&lt;/strong&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4jEs7Br" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="thermaltake-cte-c700-tg"&gt;&lt;strong&gt;Thermaltake CTE C700 TG&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.thermaltake.com/cte-c700-tg-argb-mid-tower-chassis.html" target="_blank" rel="noopener"
&gt;Thermaltake&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/45deg_hu_cc657b60b9aab3bb.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/45deg_hu_3c10c79e4ecc553a.avif 426w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/45deg_hu_fa51355b22214876.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/45deg_hu_fa51355b22214876.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/45deg_hu_564833d751c8f08d.webp 426w" width="300" height="352" alt="Thermaltake CTE C700 TG 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/hdd_hu_e5aaef5cdde1cbb5.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/hdd_hu_97a1df9afeff866f.avif 600w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/hdd_hu_d781594549e04c60.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/hdd_hu_d781594549e04c60.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/hdd_hu_f0b15da95f5253d3.webp 600w" width="300" height="248" alt="Thermaltake CTE C700 TG HDD View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/side_hu_15eaa343d7ee83f4.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/side_hu_9cb90c0c6347e4c7.avif 417w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/side_hu_e8d764b6df8e4549.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/side_hu_e8d764b6df8e4549.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/CTE_C700/side_hu_b2143c4f60adbd5b.webp 417w" width="300" height="360" alt="Thermaltake CTE C700 TG Side" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;The C700 TG isn’t really built for storage, but it brings some serious firepower if you’re building a hybrid rig. With a rotated motherboard layout for direct airflow, triple ARGB fans, and room for custom loops, it’s clearly aimed at high-performance builds. Think media storage, gaming, and VMs all in one box.&lt;/p&gt;
&lt;p&gt;It’s massive, flashy, and absolute overkill for a pure NAS. If you need a case that can handle mixed workloads and look good doing it, the C700 TG pulls it off with style. Don’t expect drive-focused design. It’s all about airflow and flexibility.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mid Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 22.3&amp;quot; × 12.9&amp;quot; × 19.8&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: E-ATX / ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 7 x 3.5&amp;quot; + 6 x 2.5&amp;quot;. Okay, but not great for dedicated NAS use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Includes 3 140mm fans. This case can support up to 11 fans. One of the best for airflow. It can also support up to 3 rads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Excellent, thanks to the dual chamber. Cables route cleanly, even with multiple drives and fans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: With high-RPM fans, it gets loud. Replace them for silence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Very solid. Premium glass and thick panels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, tempered glass, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: Heavy. Top I/O not ideal for under-desk setups.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: This case is suited for high-performance setups like gaming, media playback, VMs, or creative workstations. It’s overkill for pure storage but works if you want fancy cooling and extra headroom.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The C700 TG is no longer available on Amazon, so the buy link is gone. I&amp;rsquo;ve kept the review because the rotated-motherboard layout is worth understanding before you buy any case built around one. It moves air beautifully and it still won&amp;rsquo;t hold many drives. Everything above describes the C700 TG.&lt;/p&gt;
&lt;h2 id="phanteks-enthoo-pro-tg"&gt;&lt;strong&gt;Phanteks Enthoo Pro TG&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://phanteks.com/product/enthoo-pro-tg/" target="_blank" rel="noopener"
&gt;Phanteks&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/45deg_hu_fb80ac4b196abf5c.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/45deg_hu_111a557760154fab.avif 362w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/45deg_hu_9a0dbeb48ed669ca.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/45deg_hu_9a0dbeb48ed669ca.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/45deg_hu_db12b74aaa07a707.webp 362w" width="300" height="414" alt="Phanteks Enthoo Pro TG 45 Degree View" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side_hu_8023af5070104e33.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side_hu_3a61ffe04e5466dc.avif 494w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side_hu_aedd89da9066f98.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side_hu_aedd89da9066f98.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side_hu_a813da9f0a636f21.webp 494w" width="300" height="304" alt="Phanteks Enthoo Pro TG Side" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side2_hu_8bff82f062159e22.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side2_hu_5c2303ce7291ec2f.avif 507w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side2_hu_3f0167982d1a43be.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side2_hu_3f0167982d1a43be.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Enthoo/side2_hu_71a6816ee80940c4.webp 507w" width="300" height="296" alt="Phanteks Enthoo Pro TG Side 2" style="width: 100%; max-width: 275px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;This case is starting to show its age, but it still holds up if you manage your expectations. You get decent drive support, solid build quality, and enough room for fans. Airflow and cable management don’t match what newer cases offer.&lt;/p&gt;
&lt;p&gt;It was top-tier back in 2016. Today? It’s functional, not exceptional. If you already own it, it’s worth using. If you’re shopping for something new, there are better options on the market.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Full Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 21.1&amp;quot; × 9.3&amp;quot; × 21.7&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: SSI-EEB / E-ATX / ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 6 x 3.5&amp;quot;, 4 x 2.5&amp;quot;. Good for mid-size builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: One front fan and one rear fan are included. Airflow is okay, but you’ll want more fans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Lots of tie points and channels. Easy to keep tidy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: No insulation. Average sound profile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Decent. Nothing to write home about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, tempered glass, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: Vertical GPU mount needs a riser. Build layout is less optimized by today’s standards.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: This case is dated but dependable. Still usable if you already have it. If you&amp;rsquo;re buying new, there are better options for the price.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The Enthoo Pro TG is no longer available on Amazon, which is no great loss given where the review landed. It was a 2016 design being judged by today&amp;rsquo;s standards, and it didn&amp;rsquo;t have far to fall. The section stays as a reference for anyone who already owns one. Everything above describes the Enthoo Pro TG.&lt;/p&gt;
&lt;h2 id="-c-tier--functional-with-major-tradeoffs"&gt;🟥 C-Tier – Functional with Major Tradeoffs
&lt;/h2&gt;&lt;h2 id="corsair-7000d-airflow"&gt;&lt;strong&gt;Corsair 7000D AIRFLOW&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.corsair.com/us/en/p/pc-cases/cc-9011218-ww/7000d-airflow-full-tower-atx-pc-case-black-cc-9011218-ww" target="_blank" rel="noopener"
&gt;Corsair&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/45deg_hu_fa308929fc943a1b.avif"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/45deg_hu_7305efa4d25d4b1.webp" width="291" height="500" alt="Corsair 7000D AIRFLOW 45 Degree View" style="width: 100%; max-width: 200px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side_hu_8bfb1f2ebdcc1339.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side_hu_9cd6f9c5ba0dd91c.avif 500w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side_hu_395fcf9c17894481.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side_hu_395fcf9c17894481.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side_hu_dc46b6fd2bd67b6c.webp 500w" width="300" height="300" alt="Corsair 7000D AIRFLOW Side" style="width: 100%; max-width: 300px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side2_hu_a5334c70f894c948.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side2_hu_ad0339276c80359e.avif 500w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side2_hu_533a0a6b263924c.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side2_hu_533a0a6b263924c.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Airflow/side2_hu_7321934ba30ec1d8.webp 500w" width="300" height="300" alt="Corsair 7000D AIRFLOW Side 2" style="width: 100%; max-width: 300px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;This case is an absolute monster. Built for custom loops, high-end hardware, and airflow that won’t quit. It’s big, bold, and stunning to look at. When it comes to media server duty, it misses the mark.&lt;/p&gt;
&lt;p&gt;Drive support is the weak link. You’ll be improvising mounts by the time you hit six drives, and it’s clearly not designed with storage in mind. Unless you’re building a dual-purpose gaming and media rig, this is the wrong tool for the job. Beautiful, but not practical for a server.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Full Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 21.6&amp;quot; × 9.8&amp;quot; × 23.6&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: E-ATX / ATX / mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 6 x 3.5&amp;quot; and 4 x 2.5&amp;quot; out of the box. You’ll need adapters or mods to expand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: 3 140mm fans are included. Supports up to 12 120mm or 7 140mm fans. Top-tier airflow. Fits every radiator and fan combo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Corsair’s RapidRoute system is excellent. 36mm of routing space.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: Loud with stock fans. Quiet down with Noctua or Be Quiet swaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Premium all the way. Heavy, rigid, and beautifully finished.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, tempered glass, plastic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: Expensive, overkill for NAS use, lacking drive expansion out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: This case looks amazing and works great for gaming or workstations. For a media server, the size and cost aren’t worth it. There are better picks for storage and cooling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The 7000D AIRFLOW is no longer available on Amazon, and I&amp;rsquo;ve removed the buy link. That changes very little here, since the verdict was already to skip it for server duty. If you want a case this size, the Define 7 XL at the top of this post does the job with far better drive support. Everything above describes the 7000D AIRFLOW.&lt;/p&gt;
&lt;h2 id="cooler-master-masterbox-nr400"&gt;&lt;strong&gt;Cooler Master MasterBox NR400&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Manufacturer&amp;rsquo;s Link&lt;/strong&gt;: &lt;a class="link" href="https://www.coolermaster.com/en-us/products/masterbox-nr400-with-odd/" target="_blank" rel="noopener"
&gt;Cooler Master&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Price&lt;/strong&gt;: 💰💰💰&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 1rem;"&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/front_hu_5684cf92a40832b4.avif"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/front_hu_fc615898e29fee2e.webp" width="233" height="500" alt="Cooler Master MasterBox NR400 45 Degree View" style="max-width: 200px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/side_hu_1bbf3ee7066ba50b.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/side_hu_c2e95d637d8cddcc.avif 461w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/side_hu_581f502a28827a84.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/side_hu_581f502a28827a84.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/side_hu_9db1f58c8b7e15a9.webp 461w" width="300" height="325" alt="Cooler Master MasterBox NR400 Side" style="max-width: 300px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/topside_hu_af2e78979c2b6bb6.avif 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/topside_hu_18ed7776f838e3ba.avif 421w"&gt;&lt;img src="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/topside_hu_904a97f6289763e5.webp" srcset="https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/topside_hu_904a97f6289763e5.webp 300w, https://diymediaserver.com/post/review-best-desktop-cases-nas-build/Masterbox/topside_hu_e4eb9532f36f8b09.webp 421w" width="300" height="356" alt="Cooler Master MasterBox NR400 Top Side" style="max-width: 300px;" loading="lazy" decoding="async"&gt;&lt;/picture&gt;
&lt;/div&gt;
&lt;p&gt;The NR400 is compact, clean, and easy to work with. It has great ventilation, a solid layout, and is a breeze to build in. With support for only 4 drives, it’s not built for serious media server applications.&lt;/p&gt;
&lt;p&gt;It’s a good fit for HTPCs, small servers, or setups where storage is handled elsewhere. Planning to scale your media library? You’ll outgrow it quickly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Form Factor&lt;/strong&gt;: Mini Tower&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dimensions&lt;/strong&gt;: 16.2&amp;quot; × 8.3&amp;quot; × 16.2&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Motherboard Support&lt;/strong&gt;: mATX / Mini-ITX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: 4 x 3.5&amp;quot;/2.5&amp;quot; bays. That&amp;rsquo;s it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cooling&lt;/strong&gt;: Two 120mm fans included. Good airflow, but not enough for dense builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cable Management&lt;/strong&gt;: Space is tight. Modular PSU strongly recommended.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noise&lt;/strong&gt;: No insulation. Expect fan and drive noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Quality&lt;/strong&gt;: Not bad for a mini-tower. Side panel is a bit wobbly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Material&lt;/strong&gt;: Steel, plastic, tempered glass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pain Points&lt;/strong&gt;: Drive cage blocks PSU airflow. Limited upgrade path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Good airflow and a simple layout make it beginner-friendly, but it’s more HTPC than a server case. Best for a tiny, low-power build with storage handled elsewhere.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07PDT745F"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/cooler-master-nr400_hu_a50d3ec65ac4d1b6.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/cooler-master-nr400_hu_3d7d4e99d7fca060.webp" width="600" height="1288" alt="Cooler Master MasterBox NR400" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Cooler Master MasterBox NR400&lt;/strong&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4cZ98yV" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="final-words"&gt;Final Words
&lt;/h2&gt;&lt;p&gt;Building a home media server? Prioritize function over flashy aesthetics. That means thinking about storage, airflow, cable space, and noise before aesthetics or gaming extras. The &lt;strong&gt;Fractal Define 7 XL&lt;/strong&gt; is my top recommendation for anyone serious about capacity and silence. The &lt;strong&gt;SilverStone CS382&lt;/strong&gt; is the best plug-and-play option if you need hot-swap capabilities, now that its predecessor the CS380B is discontinued. Building on a budget? The &lt;strong&gt;Rosewill Helium&lt;/strong&gt; or &lt;strong&gt;DARKROCK Classico&lt;/strong&gt; will get you there with a little extra effort.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pick based on your storage needs, noise tolerance, and build experience. Pick right, and you only have to buy a case once.&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>Radarr Docker Compose Setup Guide (2026)</title><link>https://diymediaserver.com/post/how-to-install-radarr-in-docker/</link><pubDate>Sun, 20 Apr 2025 05:50:50 -0600</pubDate><guid>https://diymediaserver.com/post/how-to-install-radarr-in-docker/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to-install-radarr-in-docker/radarr_hu_6f90a331870c438e.webp" alt="Featured image of post Radarr Docker Compose Setup Guide (2026)" /&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;strong&gt;Default port:&lt;/strong&gt; Radarr runs on &lt;strong&gt;port 7878&lt;/strong&gt;. Access the web UI at &lt;code&gt;http://your-server-ip:7878&lt;/code&gt; once the container is running.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you love watching movies, you know the drill. Keeping a library organized and current feels like a second job. Release dates slip, quality options multiply, and sources scatter across the internet. Pretty soon your &amp;ldquo;movie collection&amp;rdquo; is a folder full of mystery files.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where &lt;strong&gt;Radarr&lt;/strong&gt; comes in. Instead of rummaging through torrent or Usenet sites for a new release, or trying to remember when something drops digitally, Radarr handles it. It watches your wanted list, grabs releases through your torrent or Usenet client, then renames, organizes, and files the result into your media library. Ready to watch on &lt;strong&gt;Jellyfin&lt;/strong&gt;, &lt;strong&gt;Plex&lt;/strong&gt;, or &lt;strong&gt;Kodi&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Set it and forget it. That&amp;rsquo;s the whole pitch.&lt;/p&gt;
&lt;h2 id="why-radarr-is-a-must-have"&gt;Why Radarr Is a Must-Have
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why Radarr is my go-to for movie automation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automated downloads.&lt;/strong&gt; Add a movie. Radarr searches for it, grabs it, and upgrades to a better release when one shows up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing movie search.&lt;/strong&gt; Radarr keeps scanning for anything missing or unavailable when you first added it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download client integration.&lt;/strong&gt; Works with &lt;strong&gt;qBittorrent&lt;/strong&gt;, &lt;strong&gt;Deluge&lt;/strong&gt;, &lt;strong&gt;SABnzbd&lt;/strong&gt;, and &lt;strong&gt;NZBGet&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Library management.&lt;/strong&gt; Clean folder structures, proper naming, full metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Radarr turns your movie library into a system that runs itself.&lt;/p&gt;
&lt;h2 id="why-run-radarr-in-docker"&gt;Why Run Radarr in Docker?
&lt;/h2&gt;&lt;p&gt;Docker is one of the best ways to run it. Here&amp;rsquo;s why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clean isolation.&lt;/strong&gt; Radarr and its dependencies stay separate from your host system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effortless updates.&lt;/strong&gt; Pull the latest image. Done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portability.&lt;/strong&gt; Move your setup to another machine without rebuilding from scratch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No dependency hell.&lt;/strong&gt; No conflicting libraries fighting with your other software.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Running Radarr in Docker keeps things tidy, predictable, and easy to troubleshoot when something breaks. And something always breaks eventually.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re ready to take control of your movie collection, this is the way.&lt;/p&gt;
&lt;h2 id="step-1-install-docker"&gt;Step 1: Install Docker
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ll need Docker installed on your server first. Walk through this guide if you haven&amp;rsquo;t already: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/" &gt;Master the Basics - How to Install Docker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-2-create-or-modify-your-docker-compose-file"&gt;Step 2: Create or Modify Your Docker Compose File
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s define your Radarr container in &lt;code&gt;docker-compose.yml&lt;/code&gt;. The file below runs Radarr on its own. Already have other Arr apps in a Compose file (say, from my &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;Sonarr guide&lt;/a&gt;&lt;/strong&gt;)? Drop the &lt;code&gt;radarr:&lt;/code&gt; service into your existing &lt;code&gt;services:&lt;/code&gt; block and reuse the same &lt;code&gt;media_network&lt;/code&gt; instead of pasting the whole thing.&lt;/p&gt;
&lt;h3 id="open-the-compose-file"&gt;Open the Compose File
&lt;/h3&gt;&lt;p&gt;Open your existing &lt;code&gt;docker-compose.yml&lt;/code&gt; or create a new one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/docker-compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# RADARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Official LinuxServer.io Radarr image&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/radarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Friendly name for the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Pulls PUID, PGID, UMASK, TZ, ports, and paths from the .env file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Maps the host port to Radarr&amp;#39;s web UI (7878) inside the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${RADARR_PORT}:7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Radarr&amp;#39;s config and database (persists across restarts)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Your movie library&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared download folder (must be the same path in every Arr container)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Lets `docker ps` show &amp;#34;healthy&amp;#34; once the UI starts answering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:7878/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Joins the shared media network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Restart automatically unless you stop it yourself&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# NETWORK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared network, created once with: docker network create media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;💡 Tip: Watch your spacing. YAML is picky. Two spaces per indent, no tabs. Mix them and the container won&amp;rsquo;t start.&lt;/p&gt;
&lt;h2 id="step-3-customize-the-env-file"&gt;Step 3: Customize the .env File
&lt;/h2&gt;&lt;h3 id="open-the-env-file"&gt;Open the .env File
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The contents should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main user ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main group ID:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# File permission mask&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;UMASK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0007&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RADARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SONARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SABNZBD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PROWLARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9696&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BAZARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6767&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DOWNLOADS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/downloads
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MEDIA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Update the Radarr paths to match your setup. Here&amp;rsquo;s what each one does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CONFIG_PATH=/docker&lt;/code&gt; is the root folder where Docker stores persistent container files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MEDIA_PATH=/media/Storage&lt;/code&gt; is the root folder for your media files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOWNLOADS_PATH=/downloads&lt;/code&gt; is where your download client drops temp and incomplete files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure your download client container shares access to the &lt;code&gt;/downloads&lt;/code&gt; path. If it doesn&amp;rsquo;t, Radarr can&amp;rsquo;t move finished downloads into your library and you&amp;rsquo;ll spend an afternoon wondering why.&lt;/p&gt;
&lt;p&gt;Also update &lt;code&gt;TZ&lt;/code&gt; to your timezone. Reference: &lt;a class="link" href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" target="_blank" rel="noopener"
&gt;this list of valid timezones&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="step-4-start-radarr"&gt;Step 4: Start Radarr
&lt;/h2&gt;&lt;p&gt;The Compose file attaches to an external network called &lt;code&gt;media_network&lt;/code&gt;, so create it once before you bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker network create media_network
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bring your new Radarr container online:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check that it&amp;rsquo;s running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;radarr&lt;/code&gt; in the list of containers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ff12a474a4fe lscr.io/linuxserver/radarr:latest &lt;span class="s2"&gt;&amp;#34;/init&amp;#34;&lt;/span&gt; &lt;span class="m"&gt;54&lt;/span&gt; seconds ago Up &lt;span class="m"&gt;53&lt;/span&gt; seconds &lt;span class="o"&gt;(&lt;/span&gt;healthy&lt;span class="o"&gt;)&lt;/span&gt; 0.0.0.0:7878-&amp;gt;7878/tcp, &lt;span class="o"&gt;[&lt;/span&gt;::&lt;span class="o"&gt;]&lt;/span&gt;:7878-&amp;gt;7878/tcp radarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-5-fix-permissions-if-needed"&gt;Step 5: Fix Permissions (If Needed)
&lt;/h2&gt;&lt;p&gt;Permission problems are the classic gotcha with media containers. Here&amp;rsquo;s how to make everything play nice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /docker/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /docker/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /media/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /media/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Want the longer explanation of what&amp;rsquo;s going on here? Read this: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/" &gt;Master the Basics - Linux Permissions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-6-access-radarr"&gt;Step 6: Access Radarr
&lt;/h2&gt;&lt;p&gt;Open your browser and hit:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://your-server-ip:7878&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;From there you can add movies, set quality profiles, and connect your download clients.&lt;/p&gt;
&lt;p&gt;For deeper configuration, the official &lt;a class="link" href="https://wiki.servarr.com/radarr" target="_blank" rel="noopener"
&gt;Radarr Wiki&lt;/a&gt; is the definitive source.&lt;/p&gt;
&lt;h2 id="step-7-keep-your-docker-software-updated"&gt;Step 7: Keep Your Docker Software Updated
&lt;/h2&gt;&lt;p&gt;To update:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull &lt;span class="c1"&gt;# Fetches the latest image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down &lt;span class="c1"&gt;# Stops and removes the running container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d &lt;span class="c1"&gt;# Starts a fresh container with the new image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Radarr comes back up on the latest version. No reconfig needed.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Radarr does the boring work. It tracks, downloads, upgrades, and organizes your movies without you babysitting it. No more hunting for files, renaming releases, or shuffling folders around by hand.&lt;/p&gt;
&lt;p&gt;Running it in Docker keeps your host system clean and your container easy to manage. Pair Radarr with &lt;strong&gt;Sonarr&lt;/strong&gt; for TV shows and &lt;strong&gt;Lidarr&lt;/strong&gt; for music, and you&amp;rsquo;re running a self-updating media library that mostly takes care of itself.&lt;/p&gt;
&lt;p&gt;Want to run the whole suite from a single Compose file instead of one app at a time? See the &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/" &gt;complete arr stack Docker Compose guide&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_c4d4ce52ea706f03.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_759bf8113c1c8e11.webp" width="100" height="100" alt="Build the complete arr stack with Docker Compose on Proxmox" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Running the whole Arr suite?&lt;/strong&gt; Stand up Prowlarr, Radarr, Sonarr, and Bazarr from one Docker Compose file, with a fast download drive and clean imports into your media pool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Build the full stack
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>How I Fixed My 24 Hour NFS Crash Loop With MergerFS LXC and Proxmox</title><link>https://diymediaserver.com/post/how-i-fixed-my-24-hour-nfs-crash-loop-with-mergerfs-lxc-and-proxmox/</link><pubDate>Fri, 11 Apr 2025 06:52:23 -0600</pubDate><guid>https://diymediaserver.com/post/how-i-fixed-my-24-hour-nfs-crash-loop-with-mergerfs-lxc-and-proxmox/</guid><description>&lt;img src="https://diymediaserver.com/post/how-i-fixed-my-24-hour-nfs-crash-loop-with-mergerfs-lxc-and-proxmox/inodes_kill_hu_a36d1ef0848d8c07.webp" alt="Featured image of post How I Fixed My 24 Hour NFS Crash Loop With MergerFS LXC and Proxmox" /&gt;&lt;p&gt;Every sysadmin, professional or weekend warrior, has that one issue that haunts them. For me, this week, it was a weird NFS crash loop on Proxmox that kicked in like clockwork. Every 24 hours, like it was on a timer. (It was. My backups were running.) The server booted, NFS shares mounted fine, media apps ran smoothly. Looked solid. Then boom. NFS would stop responding. Containers started throwing stale file handle errors and memory usage shot through the roof.&lt;/p&gt;
&lt;p&gt;I spent days chasing ghosts. If you&amp;rsquo;re running NFS exports from a MergerFS-backed VM in Proxmox, especially with LXC clients, here&amp;rsquo;s what fixed it for me and why you should check your own setup.&lt;/p&gt;
&lt;h2 id="stack-snapshot"&gt;Stack Snapshot
&lt;/h2&gt;&lt;p&gt;Before we dig into the fix, here&amp;rsquo;s what my setup looked like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proxmox&lt;/strong&gt; host with a ZFS RAIDZ1 pool&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAS VM&lt;/strong&gt; with an HBA passing through XFS drives, combined using MergerFS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MergerFS&lt;/strong&gt; presenting &lt;code&gt;/media/Storage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NFS server&lt;/strong&gt; inside the NAS VM, exporting that MergerFS mount&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LXC containers&lt;/strong&gt; (Sonarr, Jellyfin, etc.) mounting those exports as clients&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On paper, this setup checks out. Under the hood, it was primed for failure thanks to one small flag: &lt;code&gt;noforget&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="where-things-went-sideways"&gt;Where Things Went Sideways
&lt;/h2&gt;&lt;p&gt;It wasn&amp;rsquo;t obvious at first. But the symptoms started stacking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VM would freeze after &lt;strong&gt;~24 hours&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;LXC clients hit &lt;strong&gt;&amp;ldquo;stale file handle&amp;rdquo;&lt;/strong&gt; errors&lt;/li&gt;
&lt;li&gt;Slab memory ballooned, especially &lt;code&gt;fuse_inode&lt;/code&gt; (over 110,000 inodes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one was the smoking gun. Hundreds of thousands of inodes hanging around with no cleanup.&lt;/p&gt;
&lt;h2 id="wait-whats-an-inode"&gt;Wait, What&amp;rsquo;s an Inode?
&lt;/h2&gt;&lt;p&gt;Think of an inode as the metadata brain behind every file in a Linux filesystem. It stores everything about the file &lt;em&gt;except&lt;/em&gt; its name and contents. That means stuff like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File size&lt;/li&gt;
&lt;li&gt;Permissions&lt;/li&gt;
&lt;li&gt;Owner and group&lt;/li&gt;
&lt;li&gt;Timestamps&lt;/li&gt;
&lt;li&gt;Pointers to where the actual data lives on the disk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every time you create, read, or move a file, the system checks the inode to figure out what it&amp;rsquo;s dealing with. The inode number is what NFS uses to track files across the network. If those numbers change or pile up, things get ugly fast. Especially with MergerFS and NFS in the mix.&lt;/p&gt;
&lt;h2 id="smoking-gun-noforget-in-mergerfs"&gt;Smoking Gun: &lt;code&gt;noforget&lt;/code&gt; in MergerFS
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what I originally had for my MergerFS entry in &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/mnt/Pool0/Disk* /media/Storage fuse.mergerfs direct_io,defaults,allow_other,noforget,dropcacheonclose&lt;span class="o"&gt;=&lt;/span&gt;true,category.create&lt;span class="o"&gt;=&lt;/span&gt;mfs,minfreespace&lt;span class="o"&gt;=&lt;/span&gt;50G,fsname&lt;span class="o"&gt;=&lt;/span&gt;storage &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;noforget&lt;/code&gt; flag was the root of my problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; prevents the kernel from purging unused FUSE inodes. Great for certain workloads. Awful for MergerFS over NFS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; every NFS client interaction added more FUSE inodes. The kernel kept them all. Memory usage exploded. Stability collapsed.&lt;/p&gt;
&lt;h2 id="check-your-fuse_inode-count"&gt;Check Your &lt;code&gt;fuse_inode&lt;/code&gt; Count
&lt;/h2&gt;&lt;p&gt;Run this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/slabinfo &lt;span class="p"&gt;|&lt;/span&gt; grep fuse_inode
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before the fix, mine hit &lt;strong&gt;100,000+ entries&lt;/strong&gt;. After? Around &lt;strong&gt;200&lt;/strong&gt;. Immediate memory relief.&lt;/p&gt;
&lt;h2 id="the-fixes-that-saved-my-sanity-and-marriage"&gt;The Fixes That Saved My Sanity and Marriage
&lt;/h2&gt;&lt;h3 id="1-replace-noforget-with-inodecalcpath-hash"&gt;1. Replace &lt;code&gt;noforget&lt;/code&gt; with &lt;code&gt;inodecalc=path-hash&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;I pulled &lt;code&gt;noforget&lt;/code&gt; out of my MergerFS options and put &lt;code&gt;inodecalc=path-hash&lt;/code&gt; in its place. This is the key change for NFS stability. Without it, inode numbers jump around and NFS freaks out. New MergerFS line in &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/mnt/Pool0/Disk* /media/Storage fuse.mergerfs direct_io,defaults,allow_other,dropcacheonclose&lt;span class="o"&gt;=&lt;/span&gt;true,category.create&lt;span class="o"&gt;=&lt;/span&gt;mfs,minfreespace&lt;span class="o"&gt;=&lt;/span&gt;50G,inodecalc&lt;span class="o"&gt;=&lt;/span&gt;path-hash,fsname&lt;span class="o"&gt;=&lt;/span&gt;storage &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now FUSE inodes are purged normally, and NFS clients get consistent inode numbers.&lt;/p&gt;
&lt;h3 id="2-boost-vfs_cache_pressure"&gt;2. Boost &lt;code&gt;vfs_cache_pressure&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Force the kernel to clean inodes and dentries more aggressively. Add it to &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; vm.vfs_cache_pressure&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee -a /etc/sysctl.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then reboot or run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sysctl -p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-change-proxmox-disk-cache-to-writethrough"&gt;3. Change Proxmox Disk Cache to &lt;code&gt;writethrough&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;In the Proxmox VM Hardware settings, switch the disk cache mode to:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cache=writethrough&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This forces better consistency between Proxmox, ZFS, and your VM&amp;rsquo;s disk I/O.&lt;/p&gt;
&lt;h2 id="what-stability-looks-like-now"&gt;What Stability Looks Like Now
&lt;/h2&gt;&lt;p&gt;After these changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No more stale handles or NFS hangs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fuse_inode&lt;/code&gt; count is flat, around 8,000 after days of uptime&lt;/li&gt;
&lt;li&gt;Memory usage is predictable&lt;/li&gt;
&lt;li&gt;LXC clients mount and run cleanly, even under load&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quick-fix-table"&gt;Quick Fix Table
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tweak&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Remove &lt;code&gt;noforget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops memory leaks via inode bloat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add &lt;code&gt;inodecalc=path-hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents NFS from freaking out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set &lt;code&gt;vfs_cache_pressure=200&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleans up unused inode/dentry entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use &lt;code&gt;writethrough&lt;/code&gt; in Proxmox&lt;/td&gt;
&lt;td&gt;Improves disk I/O behavior with ZFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitor &lt;code&gt;/proc/slabinfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Catch inode bloat before it breaks stuff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="tldr"&gt;TL;DR
&lt;/h2&gt;&lt;p&gt;NFS + MergerFS + Proxmox VM + LXC? Check your MergerFS flags.&lt;/p&gt;
&lt;p&gt;Specifically:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ditch &lt;code&gt;noforget&lt;/code&gt;. Add &lt;code&gt;inodecalc=path-hash&lt;/code&gt;. Crank &lt;code&gt;vfs_cache_pressure&lt;/code&gt;. Set Proxmox disk cache to &lt;code&gt;writethrough&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These four config changes saved my server from daily crashes. If your NFS setup feels haunted, start here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it. Get your sanity back and sleep again.&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>Hardware - Why You Need an HBA</title><link>https://diymediaserver.com/post/hardware-why-you-need-an-hba/</link><pubDate>Sun, 06 Apr 2025 08:14:46 -0600</pubDate><guid>https://diymediaserver.com/post/hardware-why-you-need-an-hba/</guid><description>&lt;img src="https://diymediaserver.com/post/hardware-why-you-need-an-hba/hba_hu_dd78b40b6b8ce6e9.webp" alt="Featured image of post Hardware - Why You Need an HBA" /&gt;&lt;p&gt;Ever hit a wall with your media server setup? Maybe you maxed out your motherboard&amp;rsquo;s SATA ports. Or you want to hand control of your drives to a VM in Proxmox without the host getting in the way. That&amp;rsquo;s where a &lt;strong&gt;Host Bus Adapter (HBA)&lt;/strong&gt; earns its keep. It gives you smoother storage configuration, more drives, and better performance, especially if you&amp;rsquo;re running a virtualized environment.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what you need to know. When to use an HBA, why it works so well in virtual storage VMs, what cables you&amp;rsquo;ll need, and how to buy one that works out of the box.&lt;/p&gt;
&lt;h2 id="what"&gt;&lt;strong&gt;What&amp;rsquo;s an HBA and When Should You Use It?&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;A Host Bus Adapter (HBA) is a PCIe card that gives your server more drive connectivity than your motherboard can. Think of it as a high-performance SATA or SAS controller, way more capable than those cheap expansion cards that drop drives under load.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll want one if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You&amp;rsquo;re out of SATA ports.&lt;/strong&gt; Most motherboards give you six. Run a Plex or Jellyfin server and you&amp;rsquo;ll blow through those fast.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You&amp;rsquo;re using Proxmox or any hypervisor.&lt;/strong&gt; Want to pass your drives directly to a VM? You need an HBA flashed to IT mode.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You care about reliability.&lt;/strong&gt; HBAs are built to handle a pile of drives without dropping them randomly like bargain-bin SATA controllers do.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="when-to-use-an-hba"&gt;When to Use an HBA
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;HBA Fixes It By&amp;hellip;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Not enough SATA ports&lt;/td&gt;
&lt;td&gt;Expands drive capacity (8+ drives)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running Proxmox VMs&lt;/td&gt;
&lt;td&gt;Allows direct passthrough to VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheap SATA cards unreliable&lt;/td&gt;
&lt;td&gt;Provides stable, enterprise-grade control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want future expansion&lt;/td&gt;
&lt;td&gt;Supports SAS expanders (24+ drives)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="hbas-in-virtual-storage-vms-this-is-where-they-shine"&gt;&lt;strong&gt;HBAs in Virtual Storage VMs (This Is Where They Shine)&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;Running a NAS in a VM? Stop fighting your hypervisor. Pass the HBA straight to the storage VM and let it take over.&lt;/p&gt;
&lt;h3 id="1-direct-passthrough--better-performance"&gt;1. Direct Passthrough = Better Performance
&lt;/h3&gt;&lt;p&gt;Instead of juggling drive access through the host, pass the HBA to your VM. That VM gets direct, raw control over the drives. Perfect for ZFS, MergerFS, or SnapRAID.&lt;/p&gt;
&lt;h3 id="2-it-mode-no-raid-no-nonsense"&gt;2. IT Mode: No RAID, No Nonsense
&lt;/h3&gt;&lt;p&gt;Flashing the HBA to IT mode strips out the RAID firmware. The VM sees each drive on its own, the way it should. No weird RAID layer getting in the way of SMART tools or parity calculations.&lt;/p&gt;
&lt;h3 id="3-fewer-headaches-more-stability"&gt;3. Fewer Headaches, More Stability
&lt;/h3&gt;&lt;p&gt;Onboard SATA gets weird in VMs. Unreliable, slow, or flat-out flaky. An HBA gives you dedicated hardware built for this job.&lt;/p&gt;
&lt;h3 id="4-room-to-grow"&gt;4. Room to Grow
&lt;/h3&gt;&lt;p&gt;Planning for expansion? A single HBA can talk to a &lt;strong&gt;SAS expander&lt;/strong&gt; and let you run 24 or more drives through one PCIe slot.&lt;/p&gt;
&lt;h2 id="how-to-buy-an-hba-that-works-out-of-the-box"&gt;&lt;strong&gt;How to Buy an HBA That Works Out of the Box&lt;/strong&gt;
&lt;/h2&gt;&lt;h3 id="get-one-already-flashed-to-it-mode"&gt;Get One Already Flashed to IT Mode
&lt;/h3&gt;&lt;p&gt;Most used enterprise HBAs ship with RAID firmware (IR mode). You want &lt;strong&gt;IT mode&lt;/strong&gt;. That&amp;rsquo;s what makes each drive show up independently.&lt;/p&gt;
&lt;p&gt;Popular models:&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BWY1VH3V"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_a938d16ac34e6334.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_8994babafb301939.webp" width="600" height="431" alt="LSI 9211-8iB IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;LSI 9211-8iB&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Already Flashed to IT mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4iXST7J" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0CFRKNJXL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9300-8i_hu_d34514ef05caf068.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9300-8i_hu_86413786deef51f7.webp" width="600" height="449" alt="LSI 9300-8i IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;LSI 9300-8i&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Already Flashed to IT mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3R5XJU0" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B07JZ6FYVC"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/Dell-310_hu_492517212a83e091.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/Dell-310_hu_c039cd904d085684.webp" width="600" height="600" alt="Dell H310 IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Dell H310&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Already Flashed to IT mode.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3FYkcQv" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Also look for &lt;strong&gt;pre-flashed cards&lt;/strong&gt; on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;eBay (search for &amp;ldquo;IT mode HBA&amp;rdquo;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Server resellers with refurb listings&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They usually run $50 to $100. Cheap, considering the performance and stability boost.&lt;/p&gt;
&lt;h2 id="need-more-drives-get-a-sas-expander"&gt;&lt;strong&gt;Need More Drives? Get a SAS Expander&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;If 8 ports aren&amp;rsquo;t enough, plug a &lt;strong&gt;SAS expander&lt;/strong&gt; into your HBA.&lt;/p&gt;
&lt;h3 id="what-it-does"&gt;What It Does
&lt;/h3&gt;&lt;p&gt;Think of it like a network switch for hard drives. You plug the HBA into the expander, and the expander gives you dozens of ports.&lt;/p&gt;
&lt;h3 id="why-its-useful"&gt;Why It&amp;rsquo;s Useful
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;More drives per HBA&lt;/strong&gt; - Some expanders support 24 or more drives.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fewer PCIe slots used&lt;/strong&gt; - Leave room for GPUs, NICs, or other gear.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lower cost&lt;/strong&gt; - Expanders cost less than more HBAs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="popular-sas-expander"&gt;Popular SAS Expander
&lt;/h3&gt;&lt;div class="product-box"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/Intel-Expander_hu_396c62865efbe784.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/Intel-Expander_hu_8699a2bf0578db6c.webp" width="600" height="413" alt="Intel RES2SV240" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Intel RES2SV240&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Compact and solid for home labs.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://www.ebay.com/sch/i.html?_nkw=Intel&amp;#43;RES2SV240&amp;amp;mkevt=1&amp;amp;mkcid=1&amp;amp;mkrid=711-53200-19255-0&amp;amp;campid=5339130028" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Ebay&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="cables-here"&gt;&lt;strong&gt;Cables: Here&amp;rsquo;s What You Need&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;HBAs don&amp;rsquo;t use regular SATA ports. They use &lt;strong&gt;SFF connectors&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="product-box" data-asin="B088H2QLBQ"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/sff-8087_hu_51b302d89f9a5b28.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/sff-8087_hu_7efac13b7a7b21bc.webp" width="600" height="600" alt="SFF-8087 to 4x SATA" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;SFF-8087 to 4x SATA&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used for older HBAs like the LSI 9211-8i.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/42fXWsQ" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B08C2LJBLW"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/sff-8643_hu_6a88a92869df4f25.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/sff-8643_hu_935e8172ad15252c.webp" width="600" height="600" alt="SFF-8643 to 4x SATA" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;SFF-8643 to 4x SATA&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used for newer HBAs like the LSI 9300-8i.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4jkEr9h" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you&amp;rsquo;re using an expander, you&amp;rsquo;ll also need:&lt;/p&gt;
&lt;div class="product-box" data-asin="B08C28LS26"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/SFF-8087-SFF-8087_hu_67331e501c139746.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/SFF-8087-SFF-8087_hu_dcc9d4c6b5ef13db.webp" width="600" height="600" alt="SFF-8087 to SFF-8087" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;SFF-8087 to SFF-8087&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used for older HBAs like the LSI 9211-8i.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4yUt3tn" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B086TVKKNX"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/SFF-8643-SFF-8087_hu_19494113405a60ee.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/SFF-8643-SFF-8087_hu_60d42a5a79f70061.webp" width="600" height="600" alt="SFF-8643 to SFF-8087" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;SFF-8643 to SFF-8087&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Used for newer HBAs like the LSI 9300-8i.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4llbqMz" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HBA Port Type&lt;/th&gt;
&lt;th&gt;Use This Cable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SFF-8087&lt;/td&gt;
&lt;td&gt;SFF-8087 to 4x SATA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SFF-8643&lt;/td&gt;
&lt;td&gt;SFF-8643 to 4x SATA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expander Link&lt;/td&gt;
&lt;td&gt;SFF-8087 to SFF-8087&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="wrap-up-yes-you-should-use-an-hba"&gt;Wrap-Up: Yes, You Should Use an HBA
&lt;/h2&gt;&lt;p&gt;Want to scale up? Want your VM to manage your drives directly? Tired of janky SATA cards dropping disks under load? Then an HBA flashed to IT mode is a must. Pair it with an expander if you&amp;rsquo;re going big. Use the right cables. And your home media server actually scales.&lt;/p&gt;
&lt;p&gt;Before you buy, check the seller&amp;rsquo;s listing for the exact firmware revision. P20 is the standard for the LSI 9211-8i in IT mode. If the listing won&amp;rsquo;t say, assume it&amp;rsquo;s IR and budget time to reflash. And run the card in a real PCIe x8 slot, not an x1 riser, or you&amp;rsquo;ll bottleneck every drive on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skip the guesswork. Get an HBA controller flashed to IT mode.&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>SABnzbd vs NZBGet: Which NZB Downloader Should You Choose in 2026?</title><link>https://diymediaserver.com/post/choosing-the-right-usenet-file-manager/</link><pubDate>Sat, 29 Mar 2025 06:26:41 -0600</pubDate><guid>https://diymediaserver.com/post/choosing-the-right-usenet-file-manager/</guid><description>&lt;img src="https://diymediaserver.com/post/choosing-the-right-usenet-file-manager/featured_hu_489643fffa90c017.webp" alt="Featured image of post SABnzbd vs NZBGet: Which NZB Downloader Should You Choose in 2026?" /&gt;&lt;p&gt;Choosing between &lt;strong&gt;SABnzbd vs NZBGet&lt;/strong&gt; matters more than most folks realize. Back in the day, Usenet meant hours of manual unpacking, repairing, and sorting. Now SABnzbd and NZBGet do all of that for you. The hard part? Picking which one to run.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re brand new to Usenet or building your first DIY media server, this decision matters more than it looks. Pick the wrong downloader and you&amp;rsquo;ll spend nights fighting settings, broken automation, or a pegged CPU. Pick the right one and Usenet becomes the set-it-and-forget-it experience it was supposed to be.&lt;/p&gt;
&lt;p&gt;This guide breaks down the &lt;strong&gt;sabnzbd vs nzbget&lt;/strong&gt; debate from a beginner&amp;rsquo;s point of view. I&amp;rsquo;ll cover setup, day-to-day performance, automation with the Arr stack, and long-term support, so you can confidently pick the right NZB downloader for your setup in 2026.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
If you&amp;rsquo;re new to Usenet and want the least painful setup, &lt;strong&gt;SABnzbd is the winner&lt;/strong&gt;. NZBGet is faster and lighter, but SABnzbd&amp;rsquo;s guided setup, polished interface, and straightforward automation make it the better choice for most beginners.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-are-sabnzbd-and-nzbget"&gt;What Are SABnzbd and NZBGet?
&lt;/h2&gt;&lt;p&gt;Before comparing them, let&amp;rsquo;s define what they are.&lt;/p&gt;
&lt;p&gt;Both SABnzbd and NZBGet are NZB download managers, software that automates Usenet downloads.&lt;/p&gt;
&lt;p&gt;They:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download files from Usenet using NZB files&lt;/li&gt;
&lt;li&gt;Repair missing parts with PAR files&lt;/li&gt;
&lt;li&gt;Unpack archives&lt;/li&gt;
&lt;li&gt;Rename and sort files&lt;/li&gt;
&lt;li&gt;Hand everything off to apps like Sonarr or Radarr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, they&amp;rsquo;re the backbone of any Usenet-based media server.&lt;/p&gt;
&lt;p&gt;The difference is how they do it and how much effort they ask from you. And that effort adds up fast when you&amp;rsquo;re trying to get a fresh server online.&lt;/p&gt;
&lt;h2 id="sabnzbd-vs-nzbget-performance-and-system-requirements"&gt;SABnzbd vs NZBGet: Performance and System Requirements
&lt;/h2&gt;&lt;h3 id="raw-speed-and-resource-usage"&gt;Raw Speed and Resource Usage
&lt;/h3&gt;&lt;p&gt;Alright, this is the one area where NZBGet clearly wins.&lt;/p&gt;
&lt;p&gt;NZBGet is written in C++, which makes it extremely efficient. It uses less CPU, less RAM, and generally downloads faster on the same hardware. Reports from the community show noticeable speed differences, especially during heavy post-processing like repairing and unpacking large files.&lt;/p&gt;
&lt;p&gt;SABnzbd is written in Python. Python is easier to maintain and extend, but it costs more CPU cycles. During unpacking and repair, SABnzbd can spike CPU usage, which matters on weaker systems.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
On a Raspberry Pi 4 downloading a 50GB file, NZBGet typically uses 15-20% less CPU during unpacking and finishes the job 10-15% faster.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Here&amp;rsquo;s the thing though. For most people, this doesn&amp;rsquo;t matter as much as you&amp;rsquo;d think.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decisions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you&amp;rsquo;re running on a Raspberry Pi, older Intel CPU, or NAS hardware, choose &lt;strong&gt;NZBGet&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;If you have a modern mini PC, desktop CPU, or home server, &lt;strong&gt;either works fine&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even low-end Intel N100 and Ryzen mini PCs handle SABnzbd without breaking a sweat. Hardware is no longer the bottleneck for most beginners.&lt;/p&gt;
&lt;h2 id="setup-and-installation-nzbget-vs-sabnzbd"&gt;Setup and Installation: NZBGet vs SABnzbd
&lt;/h2&gt;&lt;h3 id="sabnzbd-built-for-beginners"&gt;SABnzbd: Built for Beginners
&lt;/h3&gt;&lt;p&gt;This is where SABnzbd earns its reputation.&lt;/p&gt;
&lt;p&gt;On first launch, SABnzbd walks you through a clean setup wizard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enter your Usenet provider&lt;/li&gt;
&lt;li&gt;Test the connection&lt;/li&gt;
&lt;li&gt;Pick download and completed folders&lt;/li&gt;
&lt;li&gt;Enable basic security options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can be downloading within minutes, even if you&amp;rsquo;ve never touched Usenet before. The defaults are sensible, and most users never need to touch advanced settings.&lt;/p&gt;
&lt;h3 id="nzbget-more-control-more-work"&gt;NZBGet: More Control, More Work
&lt;/h3&gt;&lt;p&gt;NZBGet installs easily, especially via Docker or NAS app stores. The challenge comes after installation.&lt;/p&gt;
&lt;p&gt;A lot of important behavior needs manual configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paths&lt;/li&gt;
&lt;li&gt;Post-processing options&lt;/li&gt;
&lt;li&gt;Security settings&lt;/li&gt;
&lt;li&gt;Script behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this is impossible, but it assumes you know what you&amp;rsquo;re configuring and why. And honestly? When you&amp;rsquo;re starting out, you usually don&amp;rsquo;t.&lt;/p&gt;
&lt;h4 id="example-setup-flow"&gt;Example setup flow:
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;SABnzbd: Install, run wizard, start downloading&lt;/li&gt;
&lt;li&gt;NZBGet: Install, configure paths, set up categories, configure post-processing, start downloading&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See the difference? SABnzbd gets you downloading in three steps. NZBGet needs five, and every one assumes you know what you&amp;rsquo;re doing.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;
A low-cost, beginner-friendly platform for running SABnzbd or NZBGet, making it ideal for novices following this guide.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A Pi 4 is the kind of hardware where NZBGet&amp;rsquo;s efficiency actually shows up in your download times. SABnzbd still runs fine on one, but you&amp;rsquo;ll feel the CPU pressure during big unpacks.&lt;/p&gt;
&lt;h2 id="web-interface-and-day-to-day-usability"&gt;Web Interface and Day-to-Day Usability
&lt;/h2&gt;&lt;h3 id="sabnzbd-interface"&gt;SABnzbd Interface
&lt;/h3&gt;&lt;p&gt;SABnzbd has one of the cleanest web interfaces in the Usenet world.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modern layout&lt;/li&gt;
&lt;li&gt;Clear queue view&lt;/li&gt;
&lt;li&gt;Multiple themes&lt;/li&gt;
&lt;li&gt;Easy access to logs and warnings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You always know what&amp;rsquo;s downloading, what failed, and why. For novices, that matters more than raw speed. Visibility prevents frustration.&lt;/p&gt;
&lt;p&gt;I can&amp;rsquo;t tell you how many times I&amp;rsquo;ve avoided a headache because SABnzbd showed me exactly what went wrong instead of making me dig through logs.&lt;/p&gt;
&lt;h3 id="nzbget-interface"&gt;NZBGet Interface
&lt;/h3&gt;&lt;p&gt;NZBGet&amp;rsquo;s interface is fast and minimal. It works well, but it assumes you already know your way around Usenet terminology.&lt;/p&gt;
&lt;p&gt;It shines when you customize it with widgets and scripts, but out of the box it feels utilitarian rather than welcoming. NZBGet will get the job done. You won&amp;rsquo;t enjoy looking at it.&lt;/p&gt;
&lt;h2 id="automation-and-arr-app-integration"&gt;Automation and Arr App Integration
&lt;/h2&gt;&lt;h3 id="sabnzbd-plug-and-play-automation"&gt;SABnzbd: Plug and Play Automation
&lt;/h3&gt;&lt;p&gt;SABnzbd plays nicely with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sonarr&lt;/li&gt;
&lt;li&gt;Radarr&lt;/li&gt;
&lt;li&gt;Lidarr&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No scripts required. Point the Arr apps at SABnzbd, map your folders, and you&amp;rsquo;re done.&lt;/p&gt;
&lt;h4 id="basic-sonarr-integration"&gt;Basic Sonarr integration:
&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Sonarr Settings → Download Clients → Add SABnzbd
Host: localhost
Port: 8080
Category: tv
Completed Download Handling: Enabled
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;SABnzbd also handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Obfuscated NZBs (those weirdly-named files that trip up other downloaders)&lt;/li&gt;
&lt;li&gt;Renaming files reliably&lt;/li&gt;
&lt;li&gt;Sorting downloads without manual rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What success looks like:&lt;/strong&gt; After adding SABnzbd to Sonarr, you&amp;rsquo;ll see a green checkmark in the download client settings. Your first episode will download, unpack, and appear in your media library without you touching anything.&lt;/p&gt;
&lt;h3 id="nzbget-powerful-but-script-driven"&gt;NZBGet: Powerful but Script-Driven
&lt;/h3&gt;&lt;p&gt;NZBGet supports everything SABnzbd does, and often more, but frequently through scripts or extra configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom post-processing&lt;/li&gt;
&lt;li&gt;Advanced RSS filters&lt;/li&gt;
&lt;li&gt;RPC API access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both tools plug into the Arr apps fine. SABnzbd tends to work out of the box. NZBGet may need category mapping or post-processing scripts for complex workflows.&lt;/p&gt;
&lt;div class="alert alert-note"&gt;
&lt;span class="alert-icon"&gt;📝&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Note:&lt;/strong&gt;
If you love tinkering, NZBGet gives you more knobs to turn. But if you want your shows to download while you sleep? SABnzbd gets you there faster.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="power-efficiency-and-background-behavior"&gt;Power Efficiency and Background Behavior
&lt;/h2&gt;&lt;p&gt;This is a subtle but potentially important difference for 24/7 systems.&lt;/p&gt;
&lt;p&gt;SABnzbd uses &lt;strong&gt;iNotify&lt;/strong&gt; on Linux, which lets it detect new files instantly without constantly scanning folders. That can cut background CPU usage and lets systems sleep when idle.&lt;/p&gt;
&lt;p&gt;NZBGet periodically rescans folders, which may:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prevent sleep modes&lt;/li&gt;
&lt;li&gt;Use more background CPU&lt;/li&gt;
&lt;li&gt;Increase power consumption on always-on systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The actual impact varies by system and configuration. On modern hardware the difference is small, but it adds up over time on power-conscious setups.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running a home server 24/7 and care about your power bill, this is worth a thought. Five watts here, ten watts there. It piles up over a year.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;
Large storage is essential for media server users downloading content with SABnzbd or NZBGet, but this drive is best for single-drive/light-duty setups, not advanced NAS.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Whichever downloader you pick, you&amp;rsquo;ll fill a drive faster than you expect. Plan storage like you mean it.&lt;/p&gt;
&lt;h2 id="long-term-support-and-updates"&gt;Long-Term Support and Updates
&lt;/h2&gt;&lt;h3 id="sabnzbd"&gt;SABnzbd
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Frequent updates&lt;/li&gt;
&lt;li&gt;Active development&lt;/li&gt;
&lt;li&gt;Large user community&lt;/li&gt;
&lt;li&gt;Excellent documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From a stability and security perspective, SABnzbd is the safer long-term choice. When something breaks, a fix usually lands within days.&lt;/p&gt;
&lt;h3 id="nzbget"&gt;NZBGet
&lt;/h3&gt;&lt;p&gt;NZBGet development slowed significantly after 2019, which spooked the community. Development has picked back up under new maintainers, but the update cadence is still less predictable.&lt;/p&gt;
&lt;p&gt;It remains stable and functional, but beginners benefit from active ecosystems. You want a tool that&amp;rsquo;s being actively maintained when you run into issues.&lt;/p&gt;
&lt;h2 id="sabnzbd-vs-nzbget-comparison-table"&gt;SABnzbd vs NZBGet Comparison Table
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;SABnzbd&lt;/th&gt;
&lt;th&gt;NZBGet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Higher resource usage&lt;/td&gt;
&lt;td&gt;Extremely lightweight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;Wizard-driven, beginner-friendly&lt;/td&gt;
&lt;td&gt;Manual configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface&lt;/td&gt;
&lt;td&gt;Polished and modern&lt;/td&gt;
&lt;td&gt;Minimal and utilitarian&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;Plug and play&lt;/td&gt;
&lt;td&gt;Script-based options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power Efficiency&lt;/td&gt;
&lt;td&gt;iNotify-based monitoring&lt;/td&gt;
&lt;td&gt;Folder rescans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;Frequent and consistent&lt;/td&gt;
&lt;td&gt;Less predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;New users, automation&lt;/td&gt;
&lt;td&gt;Low-power hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="troubleshooting-common-beginner-issues"&gt;Troubleshooting Common Beginner Issues
&lt;/h2&gt;&lt;h3 id="downloads-are-slow"&gt;Downloads Are Slow
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Check your Usenet provider&amp;rsquo;s connection limits (most cap you at 20-30 connections)&lt;/li&gt;
&lt;li&gt;Verify SSL is on (it should be, but double-check)&lt;/li&gt;
&lt;li&gt;NZBGet may outperform SABnzbd on weak CPUs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="files-fail-to-unpack"&gt;Files Fail to Unpack
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Turn on automatic repair and unpack (should be on by default in SABnzbd)&lt;/li&gt;
&lt;li&gt;Confirm enough free disk space (you need at least 2x the download size)&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t move files before post-processing finishes. Let the downloader finish its job first.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="sonarr-or-radarr-cannot-see-downloads"&gt;Sonarr or Radarr Cannot See Downloads
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Double-check folder mappings, especially in Docker (this trips up everyone at least once)&lt;/li&gt;
&lt;li&gt;Confirm completed download paths match Arr app settings exactly&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="high-cpu-usage-during-downloads"&gt;High CPU Usage During Downloads
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Normal during unpacking. That&amp;rsquo;s how PAR repair works.&lt;/li&gt;
&lt;li&gt;Consider NZBGet if you&amp;rsquo;re running on older hardware&lt;/li&gt;
&lt;li&gt;Schedule downloads for off-hours if it&amp;rsquo;s bothering you&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;
A quiet full-tower with room for a stack of drives, so a SABnzbd or NZBGet box stays tidy and has somewhere to grow. It isn&amp;rsquo;t required if you&amp;rsquo;re reusing hardware you already own.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A good case won&amp;rsquo;t make either downloader faster, but it will keep the box quiet enough that you forget it&amp;rsquo;s running. That matters when SABnzbd or NZBGet are pulling files while you&amp;rsquo;re asleep.&lt;/p&gt;
&lt;h2 id="faqs-nzbget-vs-sabnzbd-2026"&gt;FAQs: NZBGet vs SABnzbd 2026
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which is faster, NZBGet or SABnzbd?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NZBGet is typically faster and more efficient due to its C++ architecture. The difference is most noticeable on low-powered hardware.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ I have a Raspberry Pi or NAS. What should I use?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;NZBGet. It was built for low-resource systems, and you&amp;rsquo;ll notice the performance difference.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which is easier for beginners?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;SABnzbd. The setup wizard, interface, and defaults are designed for novices. You&amp;rsquo;ll be downloading in minutes instead of hours.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Can both integrate with Sonarr and Radarr?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes. SABnzbd tends to work out of the box, while NZBGet offers deeper customization options if you need them.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which one uses less power?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;SABnzbd typically uses less background power thanks to iNotify-based file monitoring, though the difference varies by setup.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which one should I pick if I&amp;#39;m unsure?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Start with SABnzbd. You can always switch later if you outgrow it, but honestly? Most people never do.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="conclusion-sabnzbd-vs-nzbget-winner"&gt;Conclusion: SABnzbd vs NZBGet Winner
&lt;/h2&gt;&lt;p&gt;After going back and forth between these two for years, I always end up back on SABnzbd.&lt;/p&gt;
&lt;p&gt;Yes, NZBGet is faster and lighter. But &lt;strong&gt;SABnzbd wins the sabnzbd vs nzbget debate&lt;/strong&gt; where it matters most for beginners:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easier setup&lt;/li&gt;
&lt;li&gt;Better interface&lt;/li&gt;
&lt;li&gt;Cleaner automation&lt;/li&gt;
&lt;li&gt;More predictable updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want something that works without babysitting, &lt;strong&gt;SABnzbd is the best choice for 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re running very limited hardware or love tweaking every detail, NZBGet still has a place. For everyone else? Start with SABnzbd and enjoy Usenet the way it was meant to work. Automatic, reliable, and quiet.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources
&lt;/h2&gt;&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;img src="https://diymediaserver.com/images/logos/sabnzbd.svg" alt="SABnzbd Official Documentation" loading="lazy"&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;SABnzbd Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://sabnzbd.org/wiki/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/nzbget_hu_c2c05ec6959b289f.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/nzbget_hu_666f9d8f4855150a.webp" width="100" height="100" alt="NZBGet Official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;NZBGet Official Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://nzbget.net/documentation"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/sonarr_hu_5952c405c1588666.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/sonarr_hu_731c7dc40b933818.webp" width="100" height="100" alt="Jellyfin Official Documentation" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Sonarr Integration Guide&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://wiki.servarr.com/sonarr"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/logos/radarr_hu_75c769ba440ff50d.avif"&gt;&lt;img src="https://diymediaserver.com/images/logos/radarr_hu_1e917ef8c9c9b40e.webp" width="100" height="100" alt="Radarr Integration Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Radarr Integration Guide&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://wiki.servarr.com/radarr"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Visit
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0BP9SNVH9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/fire-stick-max_hu_173e095fd7e59c91.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/fire-stick-max_hu_bd12c15141518ecb.webp" width="600" height="494" alt="Amazon Fire TV Stick 4K Max" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Amazon Fire TV Stick 4K Max&lt;/strong&gt;
Useful for streaming downloaded media to a TV, but not necessary for running SABnzbd/NZBGet or building the server itself.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B00vcIMPd" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Usenet vs Torrenting (2026): Speed, Privacy &amp; Cost Compared</title><link>https://diymediaserver.com/post/torrent-vs-usenet/</link><pubDate>Fri, 21 Mar 2025 06:52:48 -0600</pubDate><guid>https://diymediaserver.com/post/torrent-vs-usenet/</guid><description>&lt;img src="https://diymediaserver.com/post/torrent-vs-usenet/featured_hu_b456fde955d0489a.webp" alt="Featured image of post Usenet vs Torrenting (2026): Speed, Privacy &amp; Cost Compared" /&gt;&lt;p&gt;If you&amp;rsquo;re building a home media server with Jellyfin, Kodi, Plex, Emby, or whatever your flavor, you quickly run into the same question everyone does:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I get the media?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For most people, the choice comes down to Usenet or torrenting. They both work, are popular and they both can be automated. But they feel very different once you live with them day to day.&lt;/p&gt;
&lt;p&gt;I ran torrents for years. They worked, but between sketchy files, malware scares, dead downloads, and always needing a VPN, I eventually hit a point where I wanted something easier, even if it cost a few extra dollars a month.&lt;/p&gt;
&lt;p&gt;This post breaks down the Usenet vs torrent comparison so you can pick the right system for your media server, without hype or gatekeeping. You&amp;rsquo;ll learn which system fits your priorities (speed, cost, privacy, or automation) and what trade-offs each one brings.&lt;/p&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
If you want free and familiar, torrents still work. If you want speed, privacy, automation, and fewer headaches, Usenet usually wins for home media servers, even if it costs a bit each month.
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="quick-comparison-usenet-vs-torrent"&gt;Quick Comparison: Usenet vs Torrent
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Torrents&lt;/th&gt;
&lt;th&gt;Usenet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free (VPN ~$5/mo)&lt;/td&gt;
&lt;td&gt;~$10-15/mo total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on seeders&lt;/td&gt;
&lt;td&gt;Maxes your connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;VPN required&lt;/td&gt;
&lt;td&gt;SSL encrypted (No VPN needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good with setup&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Old content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depends on seeders&lt;/td&gt;
&lt;td&gt;Up to 18 years or more retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="usenet-vs-torrenting-what-are-you-really-choosing"&gt;Usenet vs Torrenting: What Are You Really Choosing?
&lt;/h2&gt;&lt;p&gt;Before we compare pros and cons, let&amp;rsquo;s talk about what problem each system solves.&lt;/p&gt;
&lt;h3 id="torrents-p2p-downloading"&gt;Torrents (P2P downloading)
&lt;/h3&gt;&lt;p&gt;Torrents use peer-to-peer sharing. When you download a file, you&amp;rsquo;re pulling pieces from other users (seeders) who already have it, while also uploading pieces you have to others. It&amp;rsquo;s a community effort, which sounds great until you realize that means you&amp;rsquo;re dependent on that community to share the content you want.&lt;/p&gt;
&lt;p&gt;You usually need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A torrent client like qBittorrent&lt;/li&gt;
&lt;li&gt;A tracker (public or private)&lt;/li&gt;
&lt;li&gt;A VPN if you care about privacy (unless you like getting nasty letters in the mail from your ISP)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="usenet-nzb-based-downloading"&gt;Usenet (NZB-based downloading)
&lt;/h3&gt;&lt;p&gt;Usenet uses centralized servers run by providers. Files get uploaded once, then sit on those servers for years. When you download, you pull directly from the provider using an NZB file as a map. No peers. No sharing. A direct line to the content.&lt;/p&gt;
&lt;p&gt;You usually need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Usenet provider (paid)&lt;/li&gt;
&lt;li&gt;An indexer (to find content, usually paid)&lt;/li&gt;
&lt;li&gt;A downloader like SABnzbd or NZBGet&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Look, if this sounds more complex, it can be at first. But it also unlocks a lot of automation that makes your life way easier down the road.&lt;/p&gt;
&lt;p&gt;If you want the deeper history? Check out these other posts:
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/history-torrents/torrent_hu_15570e31103c2f61.avif"&gt;&lt;img src="https://diymediaserver.com/post/history-torrents/torrent_hu_7dcf8719d51da8b3.webp" width="100" height="100" alt="History of Torrents" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;History of Torrents&lt;/strong&gt;&lt;br&gt;
How P2P Changed the Way We Share Files&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/history-torrents/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
View Article
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/history-usenet/usenet_hu_fc3623e6de1ee5b4.avif"&gt;&lt;img src="https://diymediaserver.com/post/history-usenet/usenet_hu_ca89dbab9ca0bfc9.webp" width="100" height="100" alt="History of Usenet" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;History of Usenet&lt;/strong&gt;&lt;br&gt;
And How It Powers NZB Media Downloads Today&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/history-usenet/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
View Article
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="torrents-pros-cons-and-reality-in-2026"&gt;Torrents: Pros, Cons, and Reality in 2026
&lt;/h2&gt;&lt;h3 id="pros-of-torrents"&gt;Pros of Torrents
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free to use&lt;/strong&gt; - No subscriptions needed (though you&amp;rsquo;ll want a VPN)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Huge content library&lt;/strong&gt; - Almost anything popular has been torrented&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resumable downloads&lt;/strong&gt; - Pick up where you left off, even days later&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple starting point&lt;/strong&gt; - Install client, click magnet link, done&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-of-torrents"&gt;Cons of Torrents
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s where it gets real.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed depends on seeders&lt;/strong&gt; - Few seeders means slow or dead downloads. You&amp;rsquo;ll sit there watching a 4K movie crawl at 200KB/s because three people are seeding and two of them are on dial-up. Okay, maybe not dial-up, but it feels like it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy exposure&lt;/strong&gt; - Your IP is visible to peers and trackers. Everyone can see you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Higher legal risk&lt;/strong&gt; - Copyright monitoring is easier with P2P. Those cease-and-desist letters? They come from torrent activity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead torrents happen&lt;/strong&gt; - If nobody seeds, the file is gone. I&amp;rsquo;ve spent hours hunting for a specific release only to find every torrent has zero seeders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sketchy files exist&lt;/strong&gt; - Public torrents can include malware or junk. You wanted a movie, you got a .exe file. Do not run any executable files found in torrents.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B07YP9FBMM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_84a3675c3ec53e96.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_541c32e7cf211fab.webp" width="600" height="284" alt="NVIDIA SHIELD Pro" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;NVIDIA SHIELD Pro&lt;/strong&gt;&lt;br&gt;
Acts as a robust Jellyfin/Plex client and server, making it easy to stream downloaded content to your TV.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4gZhtU2" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="usenet-nzb-pros-cons-and-why-it-feels-different"&gt;Usenet (NZB): Pros, Cons, and Why It Feels Different
&lt;/h2&gt;&lt;h3 id="pros-of-usenet"&gt;Pros of Usenet
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Very fast downloads&lt;/strong&gt; - Direct from high-bandwidth servers. You can max out your gigabit connection, every single time. No waiting, no hoping someone seeds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better privacy by default&lt;/strong&gt; - SSL encryption, no peer exposure. You&amp;rsquo;re not broadcasting to strangers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long retention&lt;/strong&gt; - Top providers offer 18+ years of file storage. That obscure show from 2008? Still there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Excellent automation&lt;/strong&gt; - Pairs seamlessly with Sonarr, Radarr, and the rest of the *arr stack. Set it and forget it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency&lt;/strong&gt; - If the NZB exists and your provider has it, it downloads. No &amp;ldquo;check back later, maybe someone will seed.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-of-usenet"&gt;Cons of Usenet
&lt;/h3&gt;&lt;p&gt;Nothing&amp;rsquo;s perfect.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It costs money&lt;/strong&gt; - $7.50-20/month for unlimited plans. For some people, that&amp;rsquo;s a dealbreaker. I get it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More pieces to set up&lt;/strong&gt; - Provider + indexer + downloader. It&amp;rsquo;s not complicated, but it&amp;rsquo;s more than &amp;ldquo;install qBittorrent.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DMCA takedowns happen&lt;/strong&gt; - Content can be removed faster than torrents. Popular stuff gets hit within hours sometimes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention is not infinite&lt;/strong&gt; - Files age out after many years.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sketchy files still exist&lt;/strong&gt; - Although I have encountered less on Usenet, they do still exist. Do not run any executable files found in NZBs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="privacy-and-safety"&gt;Privacy and Safety
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s talk about what actually protects you, because there&amp;rsquo;s a lot of misinformation out there.&lt;/p&gt;
&lt;h3 id="torrents-and-vpns"&gt;Torrents and VPNs
&lt;/h3&gt;&lt;p&gt;If you torrent, a VPN is mandatory. But here&amp;rsquo;s what most guides skip: bind your torrent client to the VPN interface. This kills downloads the moment the VPN drops (a kill switch).&lt;/p&gt;
&lt;p&gt;Without this, your real IP can leak during connection drops. And it will drop. VPNs aren&amp;rsquo;t perfect. I learned this the hard way when my ISP sent me a friendly letter about a download that happened during a very short VPN hiccup.&lt;/p&gt;
&lt;p&gt;Five minutes setting up interface binding saves you from that.&lt;/p&gt;
&lt;h3 id="usenet-privacy-limits"&gt;Usenet Privacy Limits
&lt;/h3&gt;&lt;p&gt;Usenet feels more private because you&amp;rsquo;re not sharing with peers, and SSL encrypts the connection. But let&amp;rsquo;s be clear: providers still log your activity, and your payment method links to your identity.&lt;/p&gt;
&lt;p&gt;SSL only protects data in transit, not metadata about what you downloaded. If someone really wants to know what you&amp;rsquo;re doing, they can find out. It&amp;rsquo;s way harder than with torrents, where your IP gets broadcast to every peer in the swarm.&lt;/p&gt;
&lt;h2 id="nzb-vs-torrent-indexers-and-trackers-explained"&gt;NZB vs Torrent: Indexers and Trackers Explained
&lt;/h2&gt;&lt;p&gt;This is where many beginners get confused, so let me break it down.&lt;/p&gt;
&lt;h3 id="torrent-trackers"&gt;Torrent Trackers
&lt;/h3&gt;&lt;p&gt;Trackers coordinate who has which pieces of a file. They&amp;rsquo;re like a phonebook for the swarm.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Public trackers are easy but unreliable. Anyone can use them, which means quality varies wildly.&lt;/li&gt;
&lt;li&gt;Private trackers are reliable but invite-only. You&amp;rsquo;ll need to maintain a ratio (upload as much as you download) or get kicked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trackers don&amp;rsquo;t host files. They only help peers find each other. When a tracker goes down, your torrents stop working until it comes back.&lt;/p&gt;
&lt;h3 id="usenet-indexers"&gt;Usenet Indexers
&lt;/h3&gt;&lt;p&gt;Indexers catalog Usenet posts and generate NZB files. Think of them as search engines for Usenet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Good indexers show completion rates, file health, and categories&lt;/li&gt;
&lt;li&gt;Some are free, some require invites, some cost money&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ll want at least two indexers for coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Indexers make Usenet automation work, especially with Prowlarr (which manages all your indexers in one place). Without a good indexer, Usenet is useless.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0BWY1VH3V"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_a938d16ac34e6334.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/LSI-9211-8iB_hu_8994babafb301939.webp" width="600" height="431" alt="LSI 9211-8iB IT MODE" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;LSI 9211-8iB IT MODE&lt;/strong&gt;
Useful for expanding storage with multiple drives in a DIY NAS or media server, but only needed if you plan to scale beyond a few disks.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4iXST7J" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="decision-guide-usenet-vs-torrenting-for-your-setup"&gt;Decision Guide: Usenet vs Torrenting for Your Setup
&lt;/h2&gt;&lt;p&gt;Alright, so which one&amp;rsquo;s right for you?&lt;/p&gt;
&lt;h3 id="choose-torrents-if"&gt;Choose Torrents if:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;You want &lt;strong&gt;zero monthly cost&lt;/strong&gt; and don&amp;rsquo;t mind the VPN subscription&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re comfortable using a VPN at all times (and binding it properly)&lt;/li&gt;
&lt;li&gt;You mainly download popular, well-seeded content&lt;/li&gt;
&lt;li&gt;You don&amp;rsquo;t mind occasional dead or slow downloads&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="choose-usenet-if"&gt;Choose Usenet if:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;You want &lt;strong&gt;fast, consistent downloads&lt;/strong&gt; without the seeder lottery&lt;/li&gt;
&lt;li&gt;You value privacy without mandatory VPN usage&lt;/li&gt;
&lt;li&gt;You want full automation with media management tools&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;re okay paying a small monthly fee for the convenience&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="budget-reality-check"&gt;Budget reality check
&lt;/h3&gt;&lt;p&gt;If your budget is under $10 per month, here&amp;rsquo;s what I&amp;rsquo;d do:&lt;/p&gt;
&lt;p&gt;Try Usenet free trials first. Most providers offer them. See if it works for you. If it feels too complex or limited, fall back to torrents + VPN. There&amp;rsquo;s no shame in that. Torrents still work. They&amp;rsquo;re more hands-on, that&amp;rsquo;s all.&lt;/p&gt;
&lt;h2 id="basic-setup-overview-high-level"&gt;Basic Setup Overview (High Level)
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s walk through what you&amp;rsquo;re signing up for with each system.&lt;/p&gt;
&lt;h3 id="torrent-setup-in-practice"&gt;Torrent setup in practice
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Install qBittorrent&lt;/li&gt;
&lt;li&gt;Subscribe to a VPN and bind the client to VPN interface (seriously, do this)&lt;/li&gt;
&lt;li&gt;Add trackers (public or private)&lt;/li&gt;
&lt;li&gt;Optional: connect Sonarr (TV shows) and Radarr (movies) via Prowlarr&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What success looks like:&lt;/strong&gt; You click a magnet link, the download starts immediately, and your VPN shows as connected. If you disconnect the VPN, downloads pause automatically.&lt;/p&gt;
&lt;p&gt;If downloads are slow, check seeders. If there are fewer than 10, expect problems. That&amp;rsquo;s how P2P works.&lt;/p&gt;
&lt;h3 id="usenet-setup-in-practice"&gt;Usenet setup in practice
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Choose a provider (trial first, don&amp;rsquo;t commit until you&amp;rsquo;ve tested)&lt;/li&gt;
&lt;li&gt;Join one or two indexers&lt;/li&gt;
&lt;li&gt;Install SABnzbd or NZBGet&lt;/li&gt;
&lt;li&gt;Add provider details, enable SSL&lt;/li&gt;
&lt;li&gt;Connect Sonarr and Radarr via Prowlarr&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What success looks like:&lt;/strong&gt; You add a show to Sonarr, and within minutes it&amp;rsquo;s downloading at full speed. No waiting. No checking seeders. Done.&lt;/p&gt;
&lt;p&gt;If speeds are slow, check that you&amp;rsquo;re on an unlimited plan and using enough connections (20-60 depending on provider). Some providers throttle if you&amp;rsquo;re not using their recommended settings.&lt;/p&gt;
&lt;h2 id="hybrid-approach-best-of-both-worlds"&gt;Hybrid Approach: Best of Both Worlds
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what many experienced homelab users do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Usenet as primary&lt;/strong&gt; for new and popular media&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Torrents as backup&lt;/strong&gt; for rare or niche content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives you speed and automation without losing coverage. Sonarr and Radarr can search both simultaneously, grabbing from whichever source has the best release.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been running this setup for 10 years. Usenet handles 95% of my downloads, torrents catch the rest. It&amp;rsquo;s the best of both worlds, and honestly, once you&amp;rsquo;ve got it configured, you forget it&amp;rsquo;s even there.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;
&lt;strong&gt;Must have for this build.&lt;/strong&gt;
Why it fits this post: Large, reliable storage is essential for both torrent and Usenet media libraries, and this drive offers high capacity for bulk downloads and archiving.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-common-problems"&gt;Troubleshooting Common Problems
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;re going to hit issues. Everyone does. Here&amp;rsquo;s how to fix the most common ones.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Downloads are slow&lt;/td&gt;
&lt;td&gt;Torrents&lt;/td&gt;
&lt;td&gt;Check seeders (need 5+), try different tracker, verify VPN isn&amp;rsquo;t throttling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downloads never finish&lt;/td&gt;
&lt;td&gt;Torrents&lt;/td&gt;
&lt;td&gt;Dead torrent. Look for different release with active seeders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downloads are slow&lt;/td&gt;
&lt;td&gt;Usenet&lt;/td&gt;
&lt;td&gt;Confirm unlimited plan, increase connections (20-60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NZBs fail or missing blocks&lt;/td&gt;
&lt;td&gt;Usenet&lt;/td&gt;
&lt;td&gt;DMCA takedown. Try different indexer or provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No results in Sonarr/Radarr&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Sync Prowlarr, check indexer categories, verify retention settings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="faqs-usenet-vs-torrent-questions"&gt;FAQs: Usenet vs Torrent Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What is the difference between NZB vs torrent files?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;A torrent file points to peers sharing pieces of a file. An NZB file points to Usenet servers storing those pieces. Same end result, completely different infrastructure.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Do I need a VPN with Usenet?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Usually no. SSL encryption is standard, and you&amp;rsquo;re not exposing your IP to a swarm. Some users still use a VPN for extra privacy, but it&amp;rsquo;s not required like torrenting.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Are there free Usenet providers?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;There are free tiers and trials, but they&amp;rsquo;re limited by speed, data, or retention. You&amp;rsquo;ll hit those limits fast if you&amp;rsquo;re using it. Think of free tiers as test drives, not long-term solutions.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why do torrents die, but Usenet files last years?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Torrents need active seeders. If everyone stops seeding, the file&amp;rsquo;s gone. Usenet&amp;rsquo;s files are stored on servers for a defined retention period, usually years. The file exists whether anyone&amp;rsquo;s downloading it or not.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Which is better for old or rare content?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;It depends. Usenet has long retention (18+ years on some providers), but private torrent trackers can sometimes win for ultra-niche content. If you&amp;rsquo;re looking for a specific fansub from 2005, you might need both.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="final-verdict-usenet-vs-torrenting"&gt;Final Verdict: Usenet vs Torrenting
&lt;/h2&gt;&lt;p&gt;The Usenet vs torrenting debate really comes down to what you value.&lt;/p&gt;
&lt;p&gt;Torrents are free, familiar, and still useful. But they come with trade-offs: slower speeds, privacy risks, dead files, and the constant need for a VPN.&lt;/p&gt;
&lt;p&gt;Usenet costs money, but in return you get speed, privacy, automation, and reliability. For me, that trade was worth it. I stopped worrying about sketchy downloads and let my media server do its thing.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re brand new, try both. Use trials. Break things. Learn what annoys you. Then make your decision.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s how you end up with a setup you enjoy using.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0F8JG2SHN"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/MS-A2_hu_5f3f821db0ff8b82.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/MS-A2_hu_129e8b92a5c2d003.webp" width="600" height="354" alt="MINISFORUM MS-A2" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;MINISFORUM MS-A2&lt;/strong&gt;
&lt;strong&gt;Must have for this build.&lt;/strong&gt;
Why it fits this post: A powerful, flexible mini-server that can handle torrent/Usenet clients, indexers, and media server duties with ample I/O for future growth.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4o0suZN" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>The History and Usage of Torrents</title><link>https://diymediaserver.com/post/history-torrents/</link><pubDate>Sat, 15 Mar 2025 07:29:33 -0600</pubDate><guid>https://diymediaserver.com/post/history-torrents/</guid><description>&lt;img src="https://diymediaserver.com/post/history-torrents/torrent_hu_745b23f386d8948d.webp" alt="Featured image of post The History and Usage of Torrents" /&gt;&lt;p&gt;Back in the early 2000s, the internet was changing fast, and new ways to share files were popping up everywhere. Traditional downloads had a real problem. Servers got hammered when too many people piled onto the same file at once. That&amp;rsquo;s where torrents came in. Instead of pulling a file from one source, torrents let users share pieces of a file with each other. The technology itself is 100% legal. People use it every day to distribute Linux ISOs, share public domain media, and move large files without melting a server.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve ever wondered how torrents actually work, how they&amp;rsquo;re used for media, and why everyone keeps telling you to run a VPN while downloading them, this post breaks it down.&lt;/p&gt;
&lt;h2 id="what-are-torrents-and-how-do-they-work"&gt;What Are Torrents, and How Do They Work?
&lt;/h2&gt;&lt;p&gt;A torrent is a peer-to-peer (P2P) file-sharing method built on a decentralized network. Instead of downloading from a single server, you pull bits and pieces from other users who already have the file. A &lt;strong&gt;BitTorrent client&lt;/strong&gt; coordinates the whole thing and makes sure everyone is sending and receiving pieces efficiently.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how the process works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;You grab a torrent file or magnet link.&lt;/strong&gt; This contains the metadata about the content you want.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your BitTorrent client connects to a tracker.&lt;/strong&gt; The tracker is a server that helps coordinate connections between users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You download file fragments from multiple sources.&lt;/strong&gt; Instead of one server, your client pulls bits from users all over the world.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You upload pieces while you download.&lt;/strong&gt; As you receive parts of the file, your client shares them back out. That&amp;rsquo;s what keeps the swarm fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The file is reassembled on your device.&lt;/strong&gt; Once all pieces are in, your client stitches them together into the final file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why torrenting is so fast for large files. Every downloader is also an uploader.&lt;/p&gt;
&lt;h2 id="the-rise-of-p2p-file-sharing-napster-limewire-and-kazaa"&gt;The Rise of P2P File Sharing: Napster, Limewire, and Kazaa
&lt;/h2&gt;&lt;p&gt;Before torrents took over, earlier P2P networks laid the groundwork for how digital media moves online. They were rough, but they shaped everything that came next.&lt;/p&gt;
&lt;h3 id="1-napster-1999---2001-the-beginning-of-p2p"&gt;&lt;strong&gt;1. Napster (1999 - 2001): The Beginning of P2P&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Napster was the first major P2P file-sharing service to hit the mainstream. It let users share MP3 files directly with each other, and it kicked off the first large-scale digital music piracy fight. Napster&amp;rsquo;s centralized design made it easy to use. It also made it easy to shut down. In 2001, after a brutal legal battle with the Recording Industry Association of America (RIAA), Napster was forced offline. The idea of P2P file sharing, though, was already loose in the world.&lt;/p&gt;
&lt;h3 id="2-limewire--kazaa-2001---2010-decentralization-and-the-golden-age-of-p2p"&gt;&lt;strong&gt;2. Limewire &amp;amp; Kazaa (2001 - 2010): Decentralization and the Golden Age of P2P&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;After Napster fell, the next wave of P2P networks went decentralized, which made them harder to take down. Limewire and Kazaa stepped up and used the &lt;strong&gt;Gnutella and FastTrack networks&lt;/strong&gt; to share files without a central server.&lt;/p&gt;
&lt;p&gt;These programs let users share music, movies, software, and pretty much anything else. They also had real problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Malware &amp;amp; Viruses:&lt;/strong&gt; Plenty of downloads came stuffed with spyware, adware, or outright malicious payloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fake Files &amp;amp; Corrupt Downloads:&lt;/strong&gt; No verification system meant a lot of files were mislabeled, broken, or both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legal Battles:&lt;/strong&gt; Like Napster, Limewire and Kazaa eventually got sued into the ground.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flaws and all, these services proved one thing. &lt;strong&gt;Decentralized file-sharing networks were the future.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="3-how-these-p2p-networks-shaped-modern-torrents"&gt;&lt;strong&gt;3. How These P2P Networks Shaped Modern Torrents&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;The downfall of Napster, Limewire, and Kazaa made it obvious that centralized P2P was too easy to kill. That paved the way for BitTorrent, which fixed the old models by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Using swarming.&lt;/strong&gt; Instead of pulling from one source, BitTorrent splits files into small pieces and pulls them from many users at once. That speeds transfers up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Killing the single-server dependency.&lt;/strong&gt; Unlike Napster or Kazaa, torrents don&amp;rsquo;t lean on a central authority. That makes them much harder to take down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifying file integrity.&lt;/strong&gt; Torrents use &lt;strong&gt;hash checks&lt;/strong&gt; on each piece, so corrupt or tampered chunks get caught instead of ending up in your final file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Torrents are the evolution of early P2P. They fixed most of the old problems while keeping the core idea: distributed sharing.&lt;/p&gt;
&lt;h2 id="how-are-torrents-used-for-media"&gt;How Are Torrents Used for Media?
&lt;/h2&gt;&lt;p&gt;Torrents have played a huge role in how media gets distributed online. Here are some of the most common legal and practical uses:&lt;/p&gt;
&lt;h3 id="1-distributing-open-source-software"&gt;1. &lt;strong&gt;Distributing Open-Source Software&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Plenty of open-source projects use torrents to ship their software. Linux distributions like Ubuntu and Debian offer torrents for their ISO files, which keeps the official mirrors from getting flattened on release day.&lt;/p&gt;
&lt;h3 id="2-archiving-public-domain-media"&gt;2. &lt;strong&gt;Archiving Public Domain Media&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Groups like The Internet Archive use torrents to distribute huge libraries of public domain books, movies, and music. Torrents cut their bandwidth costs, which makes them a great fit for freely available media.&lt;/p&gt;
&lt;h3 id="3-gaming-and-large-file-transfers"&gt;3. &lt;strong&gt;Gaming and Large File Transfers&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Some game developers, especially in the early days of digital distribution, used torrents to push patches or entire game files. That kept central servers alive when thousands of players hammered them for updates at the same time.&lt;/p&gt;
&lt;h3 id="4-peer-to-peer-streaming"&gt;4. &lt;strong&gt;Peer-to-Peer Streaming&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;A few platforms have experimented with torrent-based streaming, where users share video files as they watch. It reduces the load on a single hosting provider and spreads the cost across the swarm.&lt;/p&gt;
&lt;h2 id="the-dark-side-of-torrents-piracy-and-legal-issues"&gt;The Dark Side of Torrents: Piracy and Legal Issues
&lt;/h2&gt;&lt;p&gt;Torrents have plenty of legal uses. They&amp;rsquo;ve also been a piracy workhorse for two decades. Some of the most common illegal uses include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Downloading pirated movies, TV shows, and music&lt;/strong&gt; from sites like The Pirate Bay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharing cracked software and video games&lt;/strong&gt; that bypass DRM protections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributing leaked content and pre-release media&lt;/strong&gt; without authorization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spreading malware and scams&lt;/strong&gt; by disguising harmful files as legitimate downloads.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="legal-consequences-of-illegal-torrenting"&gt;&lt;strong&gt;Legal Consequences of Illegal Torrenting&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Many countries have strict anti-piracy laws, and ISPs do monitor torrent traffic. If you get caught downloading copyrighted material, you could face:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fines.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISP warnings and throttling.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lawsuits from copyright holders.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best way to stay out of trouble is to use torrents for legal purposes and pull from trusted sources.&lt;/p&gt;
&lt;h2 id="why-you-might-need-a-vpn-when-downloading-torrents"&gt;Why You Might Need a VPN When Downloading Torrents
&lt;/h2&gt;&lt;p&gt;Even if you&amp;rsquo;re only grabbing legal stuff, your ISP probably isn&amp;rsquo;t thrilled to see torrent traffic on their network. Here&amp;rsquo;s why a VPN is worth the few bucks a month:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Privacy.&lt;/strong&gt; A VPN hides your IP address from other users in the swarm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No ISP throttling.&lt;/strong&gt; Some ISPs slow torrent traffic to a crawl. A VPN gets you around that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bypass geo-restrictions.&lt;/strong&gt; Some countries block torrent sites outright. A VPN lets you reach them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anonymity on public trackers.&lt;/strong&gt; Public torrents expose your IP to thousands of strangers. A VPN cuts that risk way down.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;Torrents changed how big files move around the internet. They&amp;rsquo;re fast, efficient, and don&amp;rsquo;t fall over when one server goes down. Early P2P networks like Napster and Limewire introduced the world to decentralized sharing. Torrents took the idea and made it faster, safer, and tougher to kill.&lt;/p&gt;
&lt;p&gt;The technology is legal. &lt;strong&gt;What you download is the part that matters.&lt;/strong&gt; Stick to legal uses, and run a VPN to keep your traffic and IP private.&lt;/p&gt;</description></item><item><title>The Rise, Fall, and Rebirth of Usenet</title><link>https://diymediaserver.com/post/history-usenet/</link><pubDate>Tue, 11 Mar 2025 07:57:16 -0600</pubDate><guid>https://diymediaserver.com/post/history-usenet/</guid><description>&lt;img src="https://diymediaserver.com/post/history-usenet/usenet_hu_c63242c96f82b549.webp" alt="Featured image of post The Rise, Fall, and Rebirth of Usenet" /&gt;&lt;p&gt;Before Google, social media, or even the World Wide Web, Usenet was the backbone of online communication. It started as a way for researchers and tech enthusiasts to swap information, then grew into a sprawling, decentralized network for discussions, debates, and file sharing. It&amp;rsquo;s not the household name it used to be. But it&amp;rsquo;s still alive, and it&amp;rsquo;s still one of the best ways to pull down media using NZB files.&lt;/p&gt;
&lt;h2 id="what-is-usenet"&gt;What Is Usenet?
&lt;/h2&gt;&lt;p&gt;Usenet was built in 1979 as a decentralized system for passing messages between a network of servers. It wasn&amp;rsquo;t a web forum and it wasn&amp;rsquo;t email. Articles got copied across multiple servers so anyone could read them, and no single server owned the conversation.&lt;/p&gt;
&lt;p&gt;In the early days it was all discussions. Think of it as Reddit before Reddit existed. Topics were organized into &amp;ldquo;newsgroups&amp;rdquo; covering everything from kernel debates to music and movies. You&amp;rsquo;d post a message, the servers would fan it out, and other users would reply.&lt;/p&gt;
&lt;h2 id="the-evolution-of-usenet-from-text-to-binaries"&gt;The Evolution of Usenet: From Text to Binaries
&lt;/h2&gt;&lt;p&gt;By the 1990s, people figured out Usenet could do more than host conversations. It could move files too. That kicked off the &amp;ldquo;binary newsgroups,&amp;rdquo; where users started sharing images, software, and eventually full movies and TV shows.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the catch. Usenet wasn&amp;rsquo;t designed for file transfers. Files had to be chopped into small fragments and posted as separate articles. Miss one piece and the whole download was useless. The community came up with the &lt;strong&gt;NZB file format&lt;/strong&gt; to fix that, and it&amp;rsquo;s what made Usenet downloads fast and reliable.&lt;/p&gt;
&lt;h2 id="nzb-files-the-modern-usenet-experience"&gt;NZB Files: The Modern Usenet Experience
&lt;/h2&gt;&lt;p&gt;An NZB file works like a &lt;strong&gt;torrent file&lt;/strong&gt;, but for Usenet. Instead of hunting down hundreds of file fragments by hand, the NZB points your Usenet client at every piece and tells it to grab them all and reassemble the file.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Grab an NZB file from an &lt;strong&gt;indexer&lt;/strong&gt;, a site that catalogs Usenet content.&lt;/li&gt;
&lt;li&gt;Open the NZB in a &lt;strong&gt;Usenet client&lt;/strong&gt; like SABnzbd, NZBGet, or Newsbin.&lt;/li&gt;
&lt;li&gt;The client logs into your Usenet provider, pulls every part, and stitches the file back together.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is fast. Often faster than torrents, because Usenet providers serve you directly at high speed. No seeding, no peers, no waiting for the swarm to wake up.&lt;/p&gt;
&lt;h2 id="do-you-need-a-vpn-for-usenet"&gt;Do You Need a VPN for Usenet?
&lt;/h2&gt;&lt;p&gt;This is the question every new Usenet user asks. Short answer: it depends on what you care about and what your ISP is up to.&lt;/p&gt;
&lt;p&gt;Torrents expose your IP to every other peer in the swarm. Usenet doesn&amp;rsquo;t. You connect straight to your provider&amp;rsquo;s servers, which means your activity isn&amp;rsquo;t visible to other users. Most providers also support &lt;strong&gt;SSL encryption&lt;/strong&gt;, so your ISP can see you&amp;rsquo;re talking to a Usenet server but can&amp;rsquo;t see what you&amp;rsquo;re pulling down.&lt;/p&gt;
&lt;p&gt;So why would you still bother with a VPN?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ISP Throttling&lt;/strong&gt; - Some ISPs slow down sustained large downloads. A VPN hides what kind of traffic you&amp;rsquo;re moving and can keep your speeds intact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Additional Privacy&lt;/strong&gt; - SSL hides the contents of the connection, not the fact that you&amp;rsquo;re using Usenet. A VPN hides both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access to Restricted Content&lt;/strong&gt; - Some countries block Usenet providers or indexers outright. A VPN lets you connect from somewhere they aren&amp;rsquo;t blocked.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most people, an SSL-encrypted connection to a reputable provider is enough. If you want belt-and-suspenders privacy, layer a VPN on top.&lt;/p&gt;
&lt;h2 id="why-choose-usenet-over-torrents"&gt;Why Choose Usenet Over Torrents?
&lt;/h2&gt;&lt;p&gt;Usenet has a few real advantages over torrents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed:&lt;/strong&gt; Downloads come straight from high-speed servers, so you can saturate your connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy:&lt;/strong&gt; No peer-to-peer swarm, no IP address handed out to strangers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention:&lt;/strong&gt; Providers typically hold files for &lt;strong&gt;years&lt;/strong&gt;, sometimes more than a decade. Old releases that died on torrents are often still sitting on Usenet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="getting-started-with-usenet"&gt;Getting Started with Usenet
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what you need to get going:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A Usenet Provider&lt;/strong&gt; - Newshosting, Eweka, and UsenetServer all give you access to the newsgroups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An NZB Indexer&lt;/strong&gt; - NZBGeek, DrunkenSlug, and NZBPlanet are the usual suspects for finding NZBs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A Usenet Client&lt;/strong&gt; - SABnzbd, NZBGet, or Newsbin will download and manage the files.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pair the client with Sonarr, Radarr, or Lidarr and the whole pipeline runs itself. The *arr app queries your indexers, hands the NZB to SABnzbd or NZBGet, and the file lands in your media library without you touching it.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;Usenet doesn&amp;rsquo;t get the attention torrents or streaming services do. That&amp;rsquo;s fine. If you care about speed, retention, and not broadcasting your IP to strangers, it&amp;rsquo;s still the most efficient way to feed a media server. NZB files made the workflow approachable, and the *arr stack makes it automatic.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve never tried it, set up a provider, an indexer, and a client, and run one download end to end. You&amp;rsquo;ll see why people who use Usenet rarely go back.&lt;/p&gt;</description></item><item><title>Master the Basics Dockers Compose</title><link>https://diymediaserver.com/post/master-the-basics-docker-compose/</link><pubDate>Sat, 08 Mar 2025 05:34:38 -0800</pubDate><guid>https://diymediaserver.com/post/master-the-basics-docker-compose/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-docker-compose/docker_compose_hu_3b64933ad11c0a65.webp" alt="Featured image of post Master the Basics Dockers Compose" /&gt;&lt;p&gt;So you&amp;rsquo;ve heard about Docker and the magic of containers, but the words &amp;ldquo;YAML&amp;rdquo; and &amp;ldquo;Compose file&amp;rdquo; sound like something out of a sci-fi movie? Don&amp;rsquo;t worry. You&amp;rsquo;re in the right place. If you&amp;rsquo;re new to Docker and wondering how to use a &lt;strong&gt;docker-compose.yml&lt;/strong&gt; file to manage your containers, I&amp;rsquo;ve got you covered.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you&amp;rsquo;ll know what a &lt;strong&gt;Docker Compose file&lt;/strong&gt; is, how to use it, and why it makes running multiple Docker containers easier.&lt;/p&gt;
&lt;h2 id="what-is-a-docker-compose-file"&gt;What Is a Docker Compose File?
&lt;/h2&gt;&lt;p&gt;A &lt;strong&gt;Docker Compose file&lt;/strong&gt; is a YAML file (usually named &lt;code&gt;docker-compose.yml&lt;/code&gt;) that tells Docker how to &lt;strong&gt;run multiple containers&lt;/strong&gt; together.&lt;/p&gt;
&lt;p&gt;Think of it like a recipe. Instead of manually starting each container, setting its configurations, and linking them together one by one, you write it all down in this file. Docker does the rest.&lt;/p&gt;
&lt;h2 id="why-use-docker-compose"&gt;Why Use Docker Compose?
&lt;/h2&gt;&lt;p&gt;Docker Compose makes container management sane. Here&amp;rsquo;s why you want it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Easy multi-container setup&lt;/strong&gt; – Instead of running a wall of &lt;code&gt;docker run&lt;/code&gt; commands, you define everything in one file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Portability&lt;/strong&gt; – Share your &lt;code&gt;docker-compose.yml&lt;/code&gt; and anyone can replicate your setup.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Easier management&lt;/strong&gt; – Start, stop, or restart all your containers with a single command.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; – Configure your setup using a &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;ll break down my own &lt;code&gt;docker-compose.yml&lt;/code&gt; down. First, a quick word on what&amp;rsquo;s actually happening under the hood.&lt;/p&gt;
&lt;h2 id="what-is-orchestration"&gt;What Is Orchestration?
&lt;/h2&gt;&lt;p&gt;Orchestration is a fancy word for &lt;strong&gt;automating how different parts of your system work together&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of it like a movie production. You&amp;rsquo;ve got a pile of moving parts that all have to land at the same time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;director&lt;/strong&gt; keeps everything on schedule.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;actors&lt;/strong&gt; perform their roles based on the script.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;camera crew&lt;/strong&gt; captures the right angles.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;editors&lt;/strong&gt; put it all together in post-production.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If each person had to be hand-walked through every cue, it would be chaos. Instead, they follow a plan, and the whole thing runs without constant micromanagement.&lt;/p&gt;
&lt;p&gt;Docker Compose works the same way. You define the services once, and Docker handles the rest.&lt;/p&gt;
&lt;h2 id="yaml-formatting-rules-read-this-or-nothing-will-work"&gt;YAML Formatting Rules (Read This or Nothing Will Work)
&lt;/h2&gt;&lt;p&gt;Before you crack open a &lt;code&gt;docker-compose.yml&lt;/code&gt;, you need to understand &lt;strong&gt;YAML formatting&lt;/strong&gt;. One stray space or tab will break the whole file. Unlike JSON or XML, YAML lives and dies on indentation.&lt;/p&gt;
&lt;h3 id="yaml-rules-you-must-follow"&gt;YAML Rules You Must Follow
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use spaces, not tabs.&lt;/strong&gt; Indentation must be done using spaces. Tabs are not allowed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Consistent indentation.&lt;/strong&gt; Use the same number of spaces per level (2 or 4 is standard).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Key-value pairs are separated by colons.&lt;/strong&gt; Example: &lt;code&gt;container_name: radarr&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lists use dashes.&lt;/strong&gt; Each item in a list starts with &lt;code&gt;-&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Strings don&amp;rsquo;t need quotes (but sometimes they do).&lt;/strong&gt; Strings are usually fine bare, but wrap them in double quotes &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; if they contain special characters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Boolean values (&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;) and numbers don&amp;rsquo;t need quotes.&lt;/strong&gt; Write them as-is.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="example-of-correct-vs-incorrect-yaml-formatting"&gt;Example of Correct vs. Incorrect YAML Formatting
&lt;/h3&gt;&lt;p&gt;✅ &lt;strong&gt;Correct YAML&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;❌ &lt;strong&gt;Incorrect YAML (Tabs used instead of spaces)&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ❌ Tabs used (invalid)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;❌ &lt;strong&gt;Incorrect YAML (Inconsistent indentation)&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ❌ Indentation is off (invalid)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="breaking-down-a-docker-compose-file"&gt;Breaking Down a Docker Compose File
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s a real-world &lt;code&gt;docker-compose.yml&lt;/code&gt; that runs &lt;strong&gt;Radarr, Sonarr, Prowlarr, and SABnzbd&lt;/strong&gt;, the core tools for an automated media server. Follow this pattern and you can drop in other services without much fuss.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/radarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${RADARR_PORT}:7878&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sonarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sonarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${SONARR_PORT}:8989&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sonarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/TV:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prowlarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/prowlarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${PROWLARR_PORT}:9696&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/prowlarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sabnzbd:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${SABNZBD_PORT}:8080&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alright, let&amp;rsquo;s pick this apart.&lt;/p&gt;
&lt;h3 id="1-defining-services"&gt;1. Defining Services
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;services:&lt;/code&gt; block lists every container you want to run. Each service is a &lt;strong&gt;separate Docker container&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;radarr&lt;/code&gt; and &lt;code&gt;sonarr&lt;/code&gt; are the service names.&lt;/li&gt;
&lt;li&gt;Each service runs a specific &lt;strong&gt;Docker image&lt;/strong&gt; (&lt;code&gt;lscr.io/linuxserver/radarr&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;container_name&lt;/code&gt; sets a custom name for the running container.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-environment-variables"&gt;2. Environment Variables
&lt;/h3&gt;&lt;p&gt;Instead of hardcoding values, the Compose file pulls from an &lt;code&gt;.env&lt;/code&gt; file. That means you can change a port or a path without touching the Compose file at all.&lt;/p&gt;
&lt;p&gt;Example &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;RADARR_PORT=7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;SONARR_PORT=8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PROWLARR_PORT=9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;SABNZBD_PORT=8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;CONFIG_PATH=/path/to/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;DOWNLOADS_PATH=/path/to/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;MEDIA_PATH=/path/to/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PUID=1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PGID=1001&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;TZ=America/Denver&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tweak the &lt;code&gt;.env&lt;/code&gt; file and your whole stack picks up the change on the next &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="3-port-mapping"&gt;3. Port Mapping
&lt;/h3&gt;&lt;p&gt;Each container has ports mapped like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;${RADARR_PORT}:7878&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Port 7878 inside the container (where Radarr listens) is exposed on your host at &lt;code&gt;${RADARR_PORT}&lt;/code&gt; (set in the &lt;code&gt;.env&lt;/code&gt; file).&lt;/p&gt;
&lt;h3 id="4-volume-mounts"&gt;4. Volume Mounts
&lt;/h3&gt;&lt;p&gt;Volumes &lt;strong&gt;persist data&lt;/strong&gt; between container restarts. Skip them and you lose your settings every time the container cycles.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="5-restart-policy"&gt;5. Restart Policy
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;restart: unless-stopped&lt;/code&gt; line tells Docker to &lt;strong&gt;restart the container automatically&lt;/strong&gt; unless you manually stop it. Reboot the host, the stack comes back on its own.&lt;/p&gt;
&lt;h3 id="6-depends-on"&gt;6. Depends On
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; keyword sets the &lt;strong&gt;startup order&lt;/strong&gt; between containers. It guarantees one container starts before another, but it does &lt;strong&gt;NOT&lt;/strong&gt; wait for the dependency to be fully ready. Only that the process has launched.&lt;/p&gt;
&lt;p&gt;Confused about .env files? See my &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-dockers-env-files/" &gt;Master the Basics - Docker&amp;rsquo;s .env Files&lt;/a&gt;&lt;/strong&gt; post for the full breakdown.&lt;/p&gt;
&lt;h2 id="running-the-docker-compose-file"&gt;Running the Docker Compose File
&lt;/h2&gt;&lt;p&gt;Once your &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; files are ready, here&amp;rsquo;s the workflow:&lt;/p&gt;
&lt;h3 id="1-navigate-to-the-folder-containing-your-compose-file"&gt;1. Navigate to the folder containing your Compose file
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-start-your-containers-in-the-background"&gt;2. Start your containers in the background
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; runs the containers in &lt;strong&gt;detached mode&lt;/strong&gt;, so they keep humming along in the background instead of holding your terminal hostage.&lt;/p&gt;
&lt;h3 id="3-check-running-containers"&gt;3. Check running containers
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-stop-the-containers"&gt;4. Stop the containers
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This stops and removes the containers but &lt;strong&gt;keeps your data&lt;/strong&gt; intact, because your volumes live on the host filesystem.&lt;/p&gt;
&lt;h2 id="wrapping-it-up"&gt;Wrapping It Up
&lt;/h2&gt;&lt;p&gt;You can now spin up a full multi-container stack with one command. That&amp;rsquo;s the whole point of Compose.&lt;/p&gt;
&lt;p&gt;From here, dig into Docker networks, named volumes, and &lt;code&gt;healthcheck:&lt;/code&gt; blocks. Those are the next layers worth learning. But the file you&amp;rsquo;ve got is enough to run a real media server today.&lt;/p&gt;
&lt;p&gt;Drop the file in &lt;code&gt;/docker&lt;/code&gt;, run &lt;code&gt;docker compose up -d&lt;/code&gt;, and you&amp;rsquo;re off.&lt;/p&gt;</description></item><item><title>Master the Basics - Docker Env Files</title><link>https://diymediaserver.com/post/master-the-basics-dockers-env-files/</link><pubDate>Fri, 28 Feb 2025 07:00:25 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-dockers-env-files/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-dockers-env-files/env_files_hu_828e9fbc96e984e8.webp" alt="Featured image of post Master the Basics - Docker Env Files" /&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
A &lt;code&gt;.env&lt;/code&gt; file pulls every shared variable (PUID, PGID, TZ, ports, paths) out of your &lt;code&gt;docker-compose.yml&lt;/code&gt; into one editable file. Change one value, restart the stack, and every Arr container picks it up.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Proxmox VE&lt;/dt&gt;&lt;dd&gt;8.2&lt;/dd&gt;&lt;dt&gt;Docker&lt;/dt&gt;&lt;dd&gt;27.x&lt;/dd&gt;&lt;dt&gt;Sonarr&lt;/dt&gt;&lt;dd&gt;latest&lt;/dd&gt;&lt;dt&gt;Radarr&lt;/dt&gt;&lt;dd&gt;latest&lt;/dd&gt;&lt;dt&gt;Prowlarr&lt;/dt&gt;&lt;dd&gt;latest&lt;/dd&gt;&lt;dt&gt;SABnzbd&lt;/dt&gt;&lt;dd&gt;latest&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2025-02-28&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;h2 id="why-you-need-a-env-file"&gt;Why You Need a &lt;code&gt;.env&lt;/code&gt; File
&lt;/h2&gt;&lt;p&gt;Setting up a media server with Radarr, Sonarr, Prowlarr, and SABnzbd gets messy fast. If you&amp;rsquo;ve ever dug through your &lt;code&gt;docker-compose.yml&lt;/code&gt; to change one setting across four services, you know the pain.&lt;/p&gt;
&lt;p&gt;Hardcoding paths, user IDs, and timezone values across multiple containers makes updates a nightmare. Want to change your downloads directory? That&amp;rsquo;s at least four places you&amp;rsquo;ll need to edit. Move your media to a new drive? Worse.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where the &lt;code&gt;.env&lt;/code&gt; file comes in.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;.env&lt;/code&gt; file centralizes every shared variable into a single file. Update one value, restart the stack, and every container follows. Clean, modular, portable. I run this exact setup on my own Proxmox host with the LinuxServer.io Radarr, Sonarr, Prowlarr, and SABnzbd images, and the day I moved my media drive I edited two lines and was done.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how to create, wire up, and lock down a &lt;code&gt;.env&lt;/code&gt; file for your Arr stack.&lt;/p&gt;
&lt;h2 id="step-1-creating-the-env-file"&gt;Step 1: Creating the &lt;code&gt;.env&lt;/code&gt; File
&lt;/h2&gt;&lt;p&gt;Drop the &lt;code&gt;.env&lt;/code&gt; file in the same directory as your &lt;code&gt;docker-compose.yml&lt;/code&gt;. In previous posts the compose file lives in &lt;code&gt;/docker&lt;/code&gt;, so that&amp;rsquo;s where this one goes too.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano .env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file is hidden. Any file starting with &lt;code&gt;.&lt;/code&gt; won&amp;rsquo;t show up in a normal &lt;code&gt;ls&lt;/code&gt; (use &lt;code&gt;ls -a&lt;/code&gt; to see it).&lt;/p&gt;
&lt;p&gt;Paste this in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Main user ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PUID=1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Our media group:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PGID=1001&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;TZ=America/Denver&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;RADARR_PORT=7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;SONARR_PORT=8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;PROWLARR_PORT=9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;SABNZBD_PORT=8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;CONFIG_PATH=/docker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;DOWNLOADS_PATH=/media/Downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="l"&gt;MEDIA_PATH=/media&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="breaking-down-the-variables"&gt;Breaking Down the Variables
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PUID&lt;/code&gt; &amp;amp; &lt;code&gt;PGID&lt;/code&gt;&lt;/strong&gt; - Which user the container runs as. If you&amp;rsquo;ve ever had files created with the wrong ownership, this fixes it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TZ&lt;/code&gt;&lt;/strong&gt; - Time zone for logs and scheduled tasks (downloads, library scans).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;*_PORT&lt;/code&gt;&lt;/strong&gt; - The host port each container binds to. Change one number here instead of hunting through compose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;CONFIG_PATH&lt;/code&gt;&lt;/strong&gt; - Where each app stores its settings. Keeping all configs in one parent folder makes backups and migrations trivial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;DOWNLOADS_PATH&lt;/code&gt;&lt;/strong&gt; - Shared download directory for SABnzbd (and any other download client).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;MEDIA_PATH&lt;/code&gt;&lt;/strong&gt; - Where finished movies and TV shows live. Jellyfin, Plex, and Emby read from here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One file, one place to edit. That&amp;rsquo;s the whole point.&lt;/p&gt;
&lt;h4 id="important-note"&gt;Important Note
&lt;/h4&gt;&lt;p&gt;Make sure &lt;code&gt;CONFIG_PATH&lt;/code&gt; exists and has the right permissions before you bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir /docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="nv"&gt;$USER&lt;/span&gt;:media /docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-2-securing-the-env-file"&gt;Step 2: Securing the &lt;code&gt;.env&lt;/code&gt; File
&lt;/h2&gt;&lt;p&gt;This file holds configuration values and, if you extend it, API keys. Lock it down:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="why-this-is-important"&gt;Why This Is Important
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chmod 600&lt;/code&gt; means only the owner can read or write the file.&lt;/li&gt;
&lt;li&gt;Stops accidental edits and blocks anyone else on the box from reading it. Important the day you add a real API key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-3-using-the-env-file-in-docker-composeyml"&gt;Step 3: Using the &lt;code&gt;.env&lt;/code&gt; File in &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Now wire &lt;code&gt;docker-compose.yml&lt;/code&gt; to pull values from &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;radarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${RADARR_PORT}:7878&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/radarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Movies:/movies&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sonarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SONARR_PORT}:8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sonarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Shows:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prowlarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prowlarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${PROWLARR_PORT}:9696&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/prowlarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sabnzbd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sabnzbd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SABNZBD_PORT}:8080&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PUID=${PUID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PGID=${PGID}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=${TZ}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sabnzbd:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="how-this-works"&gt;How This Works
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;env_file: .env&lt;/code&gt; loads every variable from &lt;code&gt;.env&lt;/code&gt; into the container&amp;rsquo;s environment.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${CONFIG_PATH}&lt;/code&gt;, &lt;code&gt;${DOWNLOADS_PATH}&lt;/code&gt;, and &lt;code&gt;${MEDIA_PATH}&lt;/code&gt; replace the hardcoded paths at compose time.&lt;/li&gt;
&lt;li&gt;Moving your media to a new drive? Update &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;docker compose up -d&lt;/code&gt;. Done.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${PUID}&lt;/code&gt; and &lt;code&gt;${PGID}&lt;/code&gt; swap the user and group the containers run as.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;${TZ}&lt;/code&gt; changes the time zone everywhere at once.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="step-4-testing-and-validating-your-setup"&gt;Step 4: Testing and Validating Your Setup
&lt;/h2&gt;&lt;p&gt;Bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check the logs to confirm the variables landed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs radarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see the right paths and time zone in the startup output.&lt;/p&gt;
&lt;p&gt;You can also exec into the container and read the env directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it radarr env &lt;span class="p"&gt;|&lt;/span&gt; grep CONFIG_PATH
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that matches what&amp;rsquo;s in your &lt;code&gt;.env&lt;/code&gt;, you&amp;rsquo;re good.&lt;/p&gt;
&lt;h2 id="step-5-why-this-matters-in-the-long-run"&gt;Step 5: Why This Matters in the Long Run
&lt;/h2&gt;&lt;h3 id="1-easy-updates"&gt;1. Easy Updates
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Moving media to a new drive? Edit &lt;code&gt;.env&lt;/code&gt;, restart the stack, every container follows.&lt;/li&gt;
&lt;li&gt;Changing the time zone? One edit, not five.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-portability"&gt;2. Portability
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;New server? Copy &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; over and the stack is wired up.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-clean-and-readable-configs"&gt;3. Clean and Readable Configs
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;No more massive &lt;code&gt;docker-compose.yml&lt;/code&gt; files stuffed with hardcoded paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-security-and-best-practices"&gt;4. Security and Best Practices
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;API keys and passwords stay out of &lt;code&gt;docker-compose.yml&lt;/code&gt;, where they&amp;rsquo;re more likely to end up in a git push you regret.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="taking-it-further-advanced-env-use-cases"&gt;Taking It Further: Advanced &lt;code&gt;.env&lt;/code&gt; Use Cases
&lt;/h2&gt;&lt;p&gt;Want more out of &lt;code&gt;.env&lt;/code&gt;? A couple of patterns worth knowing.&lt;/p&gt;
&lt;h3 id="use-env-for-api-keys-carefully"&gt;Use &lt;code&gt;.env&lt;/code&gt; for API Keys (Carefully)
&lt;/h3&gt;&lt;p&gt;For services like NZBHydra or a third-party indexer, drop the API key into &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;NZBHYDRA_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;yourapikeyhere&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then reference it in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;API_KEY=${NZBHYDRA_API_KEY}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Caution:&lt;/strong&gt; never commit &lt;code&gt;.env&lt;/code&gt; to GitHub or any public repo. Add it to &lt;code&gt;.gitignore&lt;/code&gt; the moment you create it. While you&amp;rsquo;re there, add &lt;code&gt;.env.*&lt;/code&gt; too so backup copies like &lt;code&gt;.env.bak&lt;/code&gt; don&amp;rsquo;t sneak in.&lt;/p&gt;
&lt;p&gt;One file. Easy updates. No headaches.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;Once the stack is up, you can hit each container from your browser:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sonarr&lt;/strong&gt;: http://your-server-ip:8989&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Radarr&lt;/strong&gt;: http://your-server-ip:7878&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prowlarr&lt;/strong&gt;: http://your-server-ip:9696&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SABnzbd&lt;/strong&gt;: http://your-server-ip:8080&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Need your server&amp;rsquo;s IP? Run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip a &lt;span class="p"&gt;|&lt;/span&gt; grep inet
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A &lt;code&gt;.env&lt;/code&gt; file is one of the highest-leverage habits in your Docker toolbox. It keeps your Arr stack modular, easy to manage, and portable.&lt;/p&gt;
&lt;p&gt;Set it up now and stop hardcoding settings.&lt;/p&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Where does Docker Compose look for the `.env` file?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Docker Compose loads &lt;code&gt;.env&lt;/code&gt; from the same directory as the &lt;code&gt;docker-compose.yml&lt;/code&gt; file you&amp;rsquo;re running. Run &lt;code&gt;docker compose&lt;/code&gt; from &lt;code&gt;/docker&lt;/code&gt; and it picks up &lt;code&gt;/docker/.env&lt;/code&gt; automatically. If you keep your compose file elsewhere, pass &lt;code&gt;--env-file /path/to/.env&lt;/code&gt; on the command line.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Does `env_file: .env` do the same thing as variable substitution?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;No. &lt;code&gt;env_file:&lt;/code&gt; injects variables into the container&amp;rsquo;s runtime environment so the app inside sees them. &lt;code&gt;${VAR}&lt;/code&gt; substitution happens at compose parse time and rewrites the YAML before the container starts. Use substitution for ports and volume paths, and &lt;code&gt;env_file&lt;/code&gt; for variables your app reads at runtime.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I check which variables a running container actually received?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Run &lt;code&gt;docker exec -it &amp;lt;container&amp;gt; env&lt;/code&gt; to dump every variable inside the container. Pipe it through &lt;code&gt;grep&lt;/code&gt; to find a specific one, for example &lt;code&gt;docker exec -it radarr env | grep TZ&lt;/code&gt;. If a variable is missing, the &lt;code&gt;.env&lt;/code&gt; file wasn&amp;rsquo;t found or the key was misspelled in compose.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Is it safe to put API keys in a `.env` file?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Safer than putting them in &lt;code&gt;docker-compose.yml&lt;/code&gt;, but &lt;code&gt;.env&lt;/code&gt; is still plain text on disk. Lock the file with &lt;code&gt;chmod 600&lt;/code&gt;, add &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;.env.*&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, and never paste it into a screenshot or pastebin. For higher-stakes secrets, look at Docker secrets or a dedicated secrets manager.&lt;/div&gt;
&lt;/details&gt;</description></item><item><title>Master the Basics - Understanding Linux Permissions</title><link>https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/</link><pubDate>Fri, 28 Feb 2025 05:52:14 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/linux_permissions_hu_98a5c34c10fc6ea5.webp" alt="Featured image of post Master the Basics - Understanding Linux Permissions" /&gt;&lt;p&gt;Ever tried to open a file on Linux and gotten hit with a &amp;ldquo;Permission Denied&amp;rdquo; error? It’s one of those moments that makes you feel like your computer is laughing at you. You’re not alone, and it’s not as bad as it looks. Linux permissions seem cryptic at first. Once you understand the basics, you’ll be using them like a pro.&lt;/p&gt;
&lt;p&gt;Here’s how Linux permissions work, and how you fix who can read, write, or execute files on your system. By the end, you’ll know what &amp;ldquo;Permission Denied&amp;rdquo; actually means and how to make it go away.&lt;/p&gt;
&lt;h2 id="what-are-linux-permissions"&gt;What Are Linux Permissions?
&lt;/h2&gt;&lt;p&gt;Every file and directory on Linux carries a set of permissions that decide who can read, write, or execute it. That’s how the system keeps users out of each other’s stuff and out of yours. Permissions break into three categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Owner&lt;/strong&gt; – The person who created the file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; – A set of users who share permissions for the file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Others&lt;/strong&gt; – Anyone else who has access to the system.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run &lt;code&gt;ls -l&lt;/code&gt; on a file and you’ll see something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-rwxr-xr--
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Looks like gibberish. It isn’t. Here’s the breakdown:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first character (&lt;code&gt;-&lt;/code&gt;) shows the file type (&lt;code&gt;-&lt;/code&gt; for a regular file, &lt;code&gt;d&lt;/code&gt; for a directory).&lt;/li&gt;
&lt;li&gt;The next three characters (&lt;code&gt;rwx&lt;/code&gt;) are the &lt;strong&gt;Owner’s&lt;/strong&gt; permissions.&lt;/li&gt;
&lt;li&gt;The next three (&lt;code&gt;r-x&lt;/code&gt;) are for the &lt;strong&gt;Group&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The final three (&lt;code&gt;r--&lt;/code&gt;) are for &lt;strong&gt;Others&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here’s what those letters mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; – Read permission (view the file’s contents)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt; – Write permission (modify the file)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; – Execute permission (run the file as a program)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-to-read-linux-permissions"&gt;How to Read Linux Permissions
&lt;/h2&gt;&lt;p&gt;Take this example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-rwxr-xr--
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read it like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Owner&lt;/strong&gt; has &lt;code&gt;rwx&lt;/code&gt;, so they can read, write, and execute the file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; has &lt;code&gt;r-x&lt;/code&gt;, so they can read and execute, but not modify it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Others&lt;/strong&gt; have &lt;code&gt;r--&lt;/code&gt;, so they can only read the file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want to see this in action? Run this in your terminal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll see permissions, owner, group, and other file details for everything in the root directory. It’s a quick way to check who can do what.&lt;/p&gt;
&lt;h2 id="changing-permissions-with-chmod"&gt;Changing Permissions with &lt;code&gt;chmod&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;To change permissions, use the &lt;code&gt;chmod&lt;/code&gt; (change mode) command. Two ways to do it: &lt;strong&gt;Symbolic&lt;/strong&gt; and &lt;strong&gt;Numeric&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="symbolic-method"&gt;Symbolic Method
&lt;/h3&gt;&lt;p&gt;The symbolic method uses letters. Syntax:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="o"&gt;[&lt;/span&gt;who&lt;span class="o"&gt;][&lt;/span&gt;operation&lt;span class="o"&gt;][&lt;/span&gt;permissions&lt;span class="o"&gt;]&lt;/span&gt; filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;who&lt;/code&gt;: &lt;code&gt;u&lt;/code&gt; (user/owner), &lt;code&gt;g&lt;/code&gt; (group), &lt;code&gt;o&lt;/code&gt; (others), &lt;code&gt;a&lt;/code&gt; (all)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operation&lt;/code&gt;: &lt;code&gt;+&lt;/code&gt; (add), &lt;code&gt;-&lt;/code&gt; (remove), &lt;code&gt;=&lt;/code&gt; (set exactly)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;permissions&lt;/code&gt;: &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod u+x filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That gives the owner execute permission.&lt;/p&gt;
&lt;p&gt;You can stack changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod u+x,g-w filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This lets the owner execute the file while stripping write from the group.&lt;/p&gt;
&lt;h3 id="numeric-method"&gt;Numeric Method
&lt;/h3&gt;&lt;p&gt;The numeric method uses three digits, one each for Owner, Group, and Others. Each permission has a value:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; = 4&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt; = 2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; = 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add them up to get the digit you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt; (4+2+1) = Read, Write, Execute&lt;/li&gt;
&lt;li&gt;&lt;code&gt;6&lt;/code&gt; (4+2) = Read, Write&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5&lt;/code&gt; (4+1) = Read, Execute&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4&lt;/code&gt; = Read only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;755&lt;/span&gt; filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Breaks down as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7&lt;/strong&gt; (Owner: &lt;code&gt;rwx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5&lt;/strong&gt; (Group: &lt;code&gt;r-x&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5&lt;/strong&gt; (Others: &lt;code&gt;r-x&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Owner reads, writes, and executes. Group and others can read and execute. That’s it.&lt;/p&gt;
&lt;h2 id="changing-file-ownership-with-chown"&gt;Changing File Ownership with &lt;code&gt;chown&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;You can also change who owns a file using the &lt;code&gt;chown&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown newowner filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or change both owner and group at once:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown newowner:newgroup filename
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Need to apply it to a whole directory tree? Use the &lt;code&gt;-R&lt;/code&gt; option:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R newowner:newgroup directoryname
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="practical-examples"&gt;Practical Examples
&lt;/h2&gt;&lt;p&gt;Let’s put this into practice. Say you have a script called &lt;code&gt;backup.sh&lt;/code&gt;, and you want to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make it executable for yourself,&lt;/li&gt;
&lt;li&gt;Allow your group to read it but not modify or run it,&lt;/li&gt;
&lt;li&gt;Keep everyone else out entirely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s how:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod u+x,g&lt;span class="o"&gt;=&lt;/span&gt;r,o&lt;span class="o"&gt;=&lt;/span&gt; backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or with the numeric method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;750&lt;/span&gt; backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check the result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-rwxr-x---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That confirms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Owner&lt;/strong&gt; can read, write, and execute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; can read, but not write or execute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Others&lt;/strong&gt; have no access.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-understanding-permissions-matters"&gt;Why Understanding Permissions Matters
&lt;/h2&gt;&lt;p&gt;Learning Linux permissions isn’t about avoiding “Permission Denied” errors. It’s about controlling who can touch sensitive files, keeping the system from getting wrecked by accident, and holding the line on security.&lt;/p&gt;
&lt;p&gt;Linux puts you in control. That comes with a cost. Misconfigured permissions lead to security holes or system failures, so it’s worth the time to learn how they work.&lt;/p&gt;
&lt;h2 id="why-using-777-permissions-is-a-bad-idea"&gt;Why Using 777 Permissions is a Bad Idea
&lt;/h2&gt;&lt;p&gt;If you’ve spent any time searching for fixes to permission errors, you’ve probably seen the advice to “use 777.” On the surface, it sounds like a magic bullet. After all, it gives everyone full access to read, write, and execute. Problem solved, right? Not quite.&lt;/p&gt;
&lt;p&gt;Setting permissions to 777 is the equivalent of leaving your front door wide open with a neon sign that says, “Come on in, take whatever you want.” It grants read, write, and execute to everyone. Owner, Group, Others, all of them. Any user on the system can do anything they want with that file or directory. Modify it. Delete it. Run a malicious script from it. That’s a security nightmare waiting to happen.&lt;/p&gt;
&lt;p&gt;Picture this. You have a web server running a site, and you set one of the directories to 777 because it “fixes” a permission error. Congratulations. You’ve handed every user, including anonymous visitors, permission to upload and execute any file they want. Hackers love that, because it makes compromising your system trivial.&lt;/p&gt;
&lt;p&gt;It’s also risky from a stability angle. If anyone, even by accident, modifies or deletes a critical file, applications crash or the whole system falls over. You’ll be left scratching your head, wondering what went wrong.&lt;/p&gt;
&lt;p&gt;So what should you do instead? Follow the principle of least privilege. Grant only the permissions a file or directory needs. Unsure? Stop and think about who needs access and what they need to do. Web server reading files but not modifying them? Use 755. Directory where a specific group writes files? Try 775.&lt;/p&gt;
&lt;p&gt;The bottom line: &lt;strong&gt;avoid 777 unless you’re in a throwaway test environment and understand the risks.&lt;/strong&gt; In production, it’s an invitation for trouble. Set permissions properly, and you’ll save yourself a lot of headaches and maybe a few sleepless nights.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;Mastering Linux permissions is one of the first real steps toward being comfortable on the command line. It feels overwhelming at first. Stick with it, and it clicks fast.&lt;/p&gt;
&lt;p&gt;Next time you see &lt;code&gt;-rw-r--r--&lt;/code&gt;, you’ll know what it means and how to change it.&lt;/p&gt;
&lt;p&gt;Ready to try it? Open your terminal and take control of your files.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="1718503547"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/linux-basics-for-hackers_hu_41774d0cc4f297db.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/linux-basics-for-hackers_hu_d7b536fcf9207564.webp" width="600" height="797" alt="Linux Basics for Hackers, 2nd Edition" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Linux Basics for Hackers, 2nd Edition:&lt;/strong&gt; &lt;em&gt;Getting Started with Networking, Scripting, and Security in Kali&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is the book I used to learn Linux basics. I read the first edition, and this is the 2025 revision&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B01L2PwMx" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Sonarr Docker Compose Setup Guide (2026)</title><link>https://diymediaserver.com/post/how-to-install-sonarr-in-docker/</link><pubDate>Tue, 18 Feb 2025 06:24:50 -0700</pubDate><guid>https://diymediaserver.com/post/how-to-install-sonarr-in-docker/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/sonarr_hu_16410fd9e8074c58.webp" alt="Featured image of post Sonarr Docker Compose Setup Guide (2026)" /&gt;
&lt;div class="alert alert-tip"&gt;
&lt;span class="alert-icon"&gt;💡&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Tip:&lt;/strong&gt;
&lt;strong&gt;Default port:&lt;/strong&gt; Sonarr runs on &lt;strong&gt;port 8989&lt;/strong&gt;. Access the web UI at &lt;code&gt;http://your-server-ip:8989&lt;/code&gt; once the container is running.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If you&amp;rsquo;re anything like me and love watching a few good TV shows, keeping up with new episodes is a headache. The streaming landscape is chaotic. One week your favorite show is on Netflix, the next it&amp;rsquo;s pulled for an exclusive run on yet another platform. Tracking what&amp;rsquo;s available where, remembering release dates, and manually grabbing episodes turns into more work than the shows are worth.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where &lt;strong&gt;Sonarr&lt;/strong&gt; comes in. Instead of hopping between streaming platforms or hunting for downloads, Sonarr automates the whole pipeline. It watches your shows, spots new episodes the moment they&amp;rsquo;re available, and pulls them via your preferred method (torrent or Usenet). Once a file lands, Sonarr renames it, files it into your library, and makes it ready to watch on Jellyfin, Plex, or Kodi. Your shows come to you.&lt;/p&gt;
&lt;h2 id="why-sonarr-earns-its-spot-in-the-stack"&gt;Why Sonarr Earns Its Spot in the Stack
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what Sonarr actually does for you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automation.&lt;/strong&gt; No more manually searching for torrents or NZB files. Sonarr does it for you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download client integration.&lt;/strong&gt; Works with qBittorrent, Deluge, SABnzbd, and NZBGet out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Library organization.&lt;/strong&gt; Renames and sorts downloaded episodes into clean folder structures your media server can read.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quality control.&lt;/strong&gt; Want 4K for some shows and 1080p for others? Set the rules per show. Sonarr will even upgrade a file later if a better version shows up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing episode search.&lt;/strong&gt; If an episode isn&amp;rsquo;t out yet, Sonarr keeps looking until it finds a valid source.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You stop juggling websites and tools. Sonarr turns your TV collection into a hands-off pipeline.&lt;/p&gt;
&lt;h2 id="why-run-sonarr-in-docker"&gt;Why Run Sonarr in Docker?
&lt;/h2&gt;&lt;p&gt;You could install Sonarr directly on the host. But running it in Docker pays off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; Sonarr and its dependencies live in their own container, separate from the host OS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy updates.&lt;/strong&gt; &lt;code&gt;docker compose pull&lt;/code&gt; and you&amp;rsquo;re on the latest build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portability.&lt;/strong&gt; Move the whole setup to a new machine by copying the compose file and the config volume.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No dependency hell.&lt;/strong&gt; No conflicting mono versions or library mismatches to chase.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to take control of your TV library, Sonarr in Docker is the way to go.&lt;/p&gt;
&lt;h2 id="step-1-install-docker"&gt;Step 1: Install Docker
&lt;/h2&gt;&lt;p&gt;You need Docker first. If you don&amp;rsquo;t already have it, follow this guide: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/" &gt;Master the Basics - How to Install Docker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-2-create-the-sonarr-docker-compose-file"&gt;Step 2: Create the Sonarr Docker Compose File
&lt;/h2&gt;&lt;p&gt;Now set up a &lt;code&gt;docker-compose.yml&lt;/code&gt; file to define the Sonarr container.&lt;/p&gt;
&lt;h3 id="pick-a-folder"&gt;Pick a Folder
&lt;/h3&gt;&lt;p&gt;Decide where you want to keep Sonarr&amp;rsquo;s compose file. I use a root folder named &lt;code&gt;docker&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="create-the-docker-composeyml-file"&gt;Create the &lt;code&gt;docker-compose.yml&lt;/code&gt; File
&lt;/h3&gt;&lt;p&gt;Open it in your favorite editor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/docker-compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Paste this in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# SONARR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sonarr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Official LinuxServer.io Sonarr image&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lscr.io/linuxserver/sonarr:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Friendly name for the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sonarr&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Pulls PUID, PGID, UMASK, TZ, ports, and paths from the .env file&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Maps the host port to Sonarr&amp;#39;s web UI (8989) inside the container&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${SONARR_PORT}:8989&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Sonarr&amp;#39;s config and database (persists across restarts)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${CONFIG_PATH}/sonarr:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Your TV library&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${MEDIA_PATH}/Shows:/tv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared download folder (must be the same path in every Arr container)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;${DOWNLOADS_PATH}:/downloads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Lets `docker ps` show &amp;#34;healthy&amp;#34; once the UI starts answering&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;wget --no-verbose --tries=1 --spider http://localhost:8989/ping || exit 1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;start_period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;30s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Joins the shared media network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Restart automatically unless you stop it yourself&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# NETWORK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#################################&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;media_network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shared network, created once with: docker network create media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;media_network&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-3-customize-the-env-file"&gt;Step 3: Customize the .env File
&lt;/h2&gt;&lt;h3 id="open-the-env-file"&gt;Open the .env File
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano /docker/.env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The content should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# User and Group ID (Prevents permission issues)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main user ID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Main group ID:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PGID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# File permission mask&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;UMASK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0007&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Timezone (Ensures correct scheduling and logs)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Denver
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define Ports (Ports for each container are defined here)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;RADARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7878&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SONARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8989&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;SABNZBD_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PROWLARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;9696&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;BAZARR_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;6767&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data Directories (Keeps storage paths centralized)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DOWNLOADS_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/downloads
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MEDIA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/media/Storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Update the paths to match your setup. Here&amp;rsquo;s what each one does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CONFIG_PATH=/docker&lt;/code&gt; is the root folder where Docker stores persistent files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MEDIA_PATH=/media/Storage&lt;/code&gt; is the root folder for your media files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOWNLOADS_PATH=/downloads&lt;/code&gt; is the root folder where your download client drops temp and incomplete files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure the download client container shares access to the &lt;code&gt;/downloads&lt;/code&gt; path. If it doesn&amp;rsquo;t, Sonarr can&amp;rsquo;t see the finished files and the whole pipeline breaks at the import step.&lt;/p&gt;
&lt;p&gt;Also set the timezone (&lt;code&gt;TZ&lt;/code&gt;) to match yours. Pull the right string from &lt;a class="link" href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" target="_blank" rel="noopener"
&gt;this list of valid timezones&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;💡 Pro Tip: Your download client (qBittorrent, SABnzbd, whatever) needs the same &lt;code&gt;/downloads&lt;/code&gt; mount Sonarr uses. Same path, same UID/GID. Skip this and Sonarr will see a &amp;ldquo;download not found&amp;rdquo; error every time.&lt;/p&gt;
&lt;h2 id="step-4-start-sonarr"&gt;Step 4: Start Sonarr
&lt;/h2&gt;&lt;p&gt;The Compose file attaches to an external network called &lt;code&gt;media_network&lt;/code&gt;, so create it once before you bring the stack up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker network create media_network
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With everything in place, fire up the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs the container in the background (detached mode).&lt;/li&gt;
&lt;li&gt;Sonarr should now be running.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;sonarr&lt;/code&gt; in the list of running containers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;e49175ef37c5 lscr.io/linuxserver/sonarr:latest &lt;span class="s2"&gt;&amp;#34;/init&amp;#34;&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt; seconds ago Up &lt;span class="m"&gt;22&lt;/span&gt; seconds &lt;span class="o"&gt;(&lt;/span&gt;healthy&lt;span class="o"&gt;)&lt;/span&gt; 0.0.0.0:8989-&amp;gt;8989/tcp, :::8989-&amp;gt;8989/tcp sonarr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="step-5-ensure-permissions-are-set-correctly"&gt;Step 5: Ensure Permissions Are Set Correctly
&lt;/h2&gt;&lt;p&gt;Folder permissions are the single most common reason the Arr suite blows up inside Docker. Save yourself the pain. Fix the permissions on your docker and media folders before you touch the UI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /docker/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /docker/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &lt;span class="sb"&gt;`&lt;/span&gt;yourusername&lt;span class="sb"&gt;`&lt;/span&gt;:media /media/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /media/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For more on Linux permissions, read this: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-understanding-linux-permissions/" &gt;Master the Basics - Linux Permissions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="step-6-access-and-configure-sonarr"&gt;Step 6: Access and Configure Sonarr
&lt;/h2&gt;&lt;p&gt;Open a browser and go to:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;http://your-server-ip:8989
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From here you&amp;rsquo;ll wire Sonarr up to your download client and your indexers, and point it at your TV library folder. For the full configuration walkthrough, the official &lt;a class="link" href="https://wiki.servarr.com/sonarr" target="_blank" rel="noopener"
&gt;Sonarr Wiki&lt;/a&gt; is the source of truth.&lt;/p&gt;
&lt;h2 id="step-7-keeping-sonarr-updated"&gt;Step 7: Keeping Sonarr Updated
&lt;/h2&gt;&lt;p&gt;One of Docker&amp;rsquo;s biggest wins is painless updates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose pull &lt;span class="c1"&gt;# Fetches the latest image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose down &lt;span class="c1"&gt;# Stops and removes the running container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d &lt;span class="c1"&gt;# Starts a fresh container with the new image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run that every month or so. You&amp;rsquo;ll stay on top of features and security patches without ever opening Sonarr&amp;rsquo;s UI.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;Sonarr handles the boring parts of running a TV library. It watches, downloads, renames, and files episodes so you don&amp;rsquo;t have to. In Docker, you get all that without polluting the host OS, and updating it is three commands.&lt;/p&gt;
&lt;p&gt;Next, point Sonarr at an indexer (NZBGeek, Drunkenslug, or your tracker of choice) and a download client. Once shows start flowing in, add Radarr for movies and Lidarr for music. Same compose file, same &lt;code&gt;.env&lt;/code&gt;, same network. That&amp;rsquo;s how the Arr stack snowballs into a full media automation rig. If you&amp;rsquo;d rather build the whole thing at once, follow the &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/" &gt;complete arr stack Docker Compose guide&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_c4d4ce52ea706f03.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_759bf8113c1c8e11.webp" width="100" height="100" alt="Build the complete arr stack with Docker Compose on Proxmox" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Running the whole Arr suite?&lt;/strong&gt; Stand up Prowlarr, Radarr, Sonarr, and Bazarr from one Docker Compose file, with a fast download drive and clean imports into your media pool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Build the full stack
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Master The Basics - Linux File Folders - What They Do and Why They Matter</title><link>https://diymediaserver.com/post/master-the-basics-understanding-linux-file-folders/</link><pubDate>Sun, 16 Feb 2025 07:15:05 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-understanding-linux-file-folders/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-understanding-linux-file-folders/linux_file_folders_hu_edf6e3bead69af65.webp" alt="Featured image of post Master The Basics - Linux File Folders - What They Do and Why They Matter" /&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
Linux puts everything under a single root (&lt;code&gt;/&lt;/code&gt;) tree, and each top-level folder has a job. Learn the ten that matter and you can troubleshoot, manage, and break things on purpose with a lot more confidence.
&lt;/div&gt;
&lt;/div&gt;
&lt;aside class="tested-on" aria-label="Tested configuration"&gt;
&lt;div class="tested-on-label"&gt;Tested on&lt;/div&gt;
&lt;dl class="tested-on-list"&gt;&lt;dt&gt;Linux&lt;/dt&gt;&lt;dd&gt;Debian 12 / Ubuntu 22.04&lt;/dd&gt;&lt;dt&gt;Date&lt;/dt&gt;&lt;dd&gt;2025-02-16&lt;/dd&gt;&lt;/dl&gt;
&lt;/aside&gt;
&lt;p&gt;New to Linux? The file system can look like a wall of cryptic two-letter folders the first time you &lt;code&gt;ls /&lt;/code&gt;. Windows hands you drive letters (C:, D:, E:). Linux hands you one tree. Everything, and I mean everything, lives somewhere under the &lt;strong&gt;root (&lt;code&gt;/&lt;/code&gt;) directory&lt;/strong&gt;, including your USB stick, your CPU info, and the password file.&lt;/p&gt;
&lt;p&gt;Every folder under &lt;code&gt;/&lt;/code&gt; has a job. Once you know what each one is for, troubleshooting stops feeling like guesswork. I&amp;rsquo;ve poked through these directories on Debian 12 and Ubuntu 22.04 servers for years, so here&amp;rsquo;s the tour I wish someone had given me on day one. Ten directories. What they do, what&amp;rsquo;s inside, and why you should care.&lt;/p&gt;
&lt;h2 id="1----the-root-directory"&gt;1. &lt;code&gt;/&lt;/code&gt; - The Root Directory
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The root directory (&lt;code&gt;/&lt;/code&gt;) is the top of the Linux file tree. Every file, folder, and mounted device hangs off of it somewhere.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s the base that holds every other directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows scatters programs and data across drives. Linux puts the whole system under &lt;code&gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The installer builds this layout for you. You don&amp;rsquo;t create it by hand.&lt;/li&gt;
&lt;li&gt;Delete or corrupt the wrong thing inside &lt;code&gt;/&lt;/code&gt; and your system stops booting. Be careful up here.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-home---user-home-directories"&gt;2. &lt;code&gt;/home&lt;/code&gt; - User Home Directories
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/home&lt;/code&gt; holds a personal directory for every user account on the system.&lt;/li&gt;
&lt;li&gt;If your username is &lt;code&gt;john&lt;/code&gt;, your stuff lives in &lt;code&gt;/home/john/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Personal files:&lt;/strong&gt; Documents, downloads, pictures, music, videos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration files:&lt;/strong&gt; Hidden &amp;ldquo;dotfiles&amp;rdquo; like &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.profile&lt;/code&gt; that hold per-user settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-specific app data:&lt;/strong&gt; Folders like &lt;code&gt;~/.config/&lt;/code&gt; and &lt;code&gt;~/.local/share/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows hides user data in &lt;code&gt;C:\Users\&lt;/code&gt;. Linux keeps it clean inside &lt;code&gt;/home&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put &lt;code&gt;/home&lt;/code&gt; on its own partition and you can wipe and reinstall the OS without losing your files. Worth doing on any serious build.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-root---the-root-users-home-directory"&gt;3. &lt;code&gt;/root&lt;/code&gt; - The Root User&amp;rsquo;s Home Directory
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/root&lt;/code&gt; is the home directory for the &lt;strong&gt;root user&lt;/strong&gt;, the superuser who can do anything.&lt;/li&gt;
&lt;li&gt;It sits separate from regular accounts (&lt;code&gt;/home/username/&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Root&amp;rsquo;s personal config files and dotfiles.&lt;/li&gt;
&lt;li&gt;Admin scripts and commands that only root should run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Regular users can&amp;rsquo;t read or write &lt;code&gt;/root&lt;/code&gt;. That&amp;rsquo;s the point. It keeps daily-driver accounts away from things that can hose the system.&lt;/li&gt;
&lt;li&gt;If you ever log in as root (don&amp;rsquo;t, use &lt;code&gt;sudo&lt;/code&gt;), this is your home.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="4-etc---system-configuration-files"&gt;4. &lt;code&gt;/etc&lt;/code&gt; - System Configuration Files
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc&lt;/code&gt; holds the &lt;strong&gt;system-wide configuration files&lt;/strong&gt; that tell Linux and its services how to behave.&lt;/li&gt;
&lt;li&gt;Almost every service you install drops a config here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Network settings:&lt;/strong&gt; &lt;code&gt;/etc/network/interfaces&lt;/code&gt; or &lt;code&gt;/etc/netplan/&lt;/code&gt; for network config, &lt;code&gt;/etc/hosts&lt;/code&gt; for local hostnames.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User accounts:&lt;/strong&gt; &lt;code&gt;/etc/passwd&lt;/code&gt; for accounts, &lt;code&gt;/etc/shadow&lt;/code&gt; for password hashes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service configs:&lt;/strong&gt; &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;, &lt;code&gt;/etc/fstab&lt;/code&gt; for mounts, firewall rules, web server configs, the lot.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a service is misbehaving, the answer is almost always in &lt;code&gt;/etc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Edit the wrong line in the wrong file and you can lock yourself out or refuse to boot. Back up the file before you touch it: &lt;code&gt;sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="5-bin-and-sbin---essential-system-programs"&gt;5. &lt;code&gt;/bin&lt;/code&gt; and &lt;code&gt;/sbin&lt;/code&gt; - Essential System Programs
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What they do:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/bin&lt;/code&gt; (binaries) and &lt;code&gt;/sbin&lt;/code&gt; (system binaries) hold the core programs that make Linux usable.&lt;/li&gt;
&lt;li&gt;Heads up: on modern Debian, Ubuntu, and most distros, &lt;code&gt;/bin&lt;/code&gt; and &lt;code&gt;/sbin&lt;/code&gt; are symlinks to &lt;code&gt;/usr/bin&lt;/code&gt; and &lt;code&gt;/usr/sbin&lt;/code&gt;. Same files, different paths. Run &lt;code&gt;ls -l /bin&lt;/code&gt; and you&amp;rsquo;ll see the link.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside &lt;code&gt;/bin&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basic commands every user needs:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ls&lt;/code&gt; (list files)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp&lt;/code&gt; (copy files)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm&lt;/code&gt; (remove files)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat&lt;/code&gt; (view file contents)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside &lt;code&gt;/sbin&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin commands, usually root-only:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shutdown&lt;/code&gt; (turn off the system)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fdisk&lt;/code&gt; (partition disks)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iptables&lt;/code&gt; / &lt;code&gt;nft&lt;/code&gt; (firewall rules)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why they matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These are the tools the system itself needs to boot and recover.&lt;/li&gt;
&lt;li&gt;Lose &lt;code&gt;/bin&lt;/code&gt; and you can&amp;rsquo;t run &lt;code&gt;ls&lt;/code&gt;. You&amp;rsquo;re in single-user-mode-with-a-rescue-USB territory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="6-var---variable-data-logs-caches-databases"&gt;6. &lt;code&gt;/var&lt;/code&gt; - Variable Data (Logs, Caches, Databases)
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/var&lt;/code&gt; holds files that &lt;strong&gt;change all the time&lt;/strong&gt;: logs, caches, mail queues, package data, database files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logs:&lt;/strong&gt; &lt;code&gt;/var/log/&lt;/code&gt; holds &lt;code&gt;syslog&lt;/code&gt;, &lt;code&gt;auth.log&lt;/code&gt;, &lt;code&gt;journal/&lt;/code&gt;, service-specific logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web roots:&lt;/strong&gt; &lt;code&gt;/var/www/&lt;/code&gt; is the default home for Nginx and Apache sites.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spools:&lt;/strong&gt; &lt;code&gt;/var/spool/&lt;/code&gt; holds print queues and mail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package data:&lt;/strong&gt; &lt;code&gt;/var/lib/&lt;/code&gt; and &lt;code&gt;/var/cache/apt/&lt;/code&gt; hold dpkg and apt state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server acting weird? Start in &lt;code&gt;/var/log/&lt;/code&gt;. The answer is almost always in there. Run &lt;code&gt;sudo journalctl -xe&lt;/code&gt; or &lt;code&gt;sudo tail -f /var/log/syslog&lt;/code&gt; and read.&lt;/li&gt;
&lt;li&gt;Fill &lt;code&gt;/var&lt;/code&gt; and things break in ugly ways. Runaway logs can crash databases, web servers, and mail. Watch &lt;code&gt;df -h /var&lt;/code&gt; on any box that&amp;rsquo;s been up a while.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="7-usr---user-installed-programs-and-libraries"&gt;7. &lt;code&gt;/usr&lt;/code&gt; - User-installed Programs and Libraries
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr&lt;/code&gt; holds the bulk of installed software, the libraries those programs need, and shared system data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/bin/&lt;/code&gt;: Most of the commands you actually use day-to-day (&lt;code&gt;nano&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/sbin/&lt;/code&gt;: Non-essential admin tools.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/&lt;/code&gt;: Shared libraries the binaries depend on.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/&lt;/code&gt;: Software you install by hand outside the package manager. Anything you compile from source usually lands in &lt;code&gt;/usr/local/bin/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed a tool from source and can&amp;rsquo;t find it? Try &lt;code&gt;/usr/local/bin/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The split between &lt;code&gt;/bin&lt;/code&gt; and &lt;code&gt;/usr/bin&lt;/code&gt; was historical. With usrmerge, the distinction is mostly cosmetic on modern systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="8-tmp---temporary-files"&gt;8. &lt;code&gt;/tmp&lt;/code&gt; - Temporary Files
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/tmp&lt;/code&gt; holds scratch files that the system &lt;strong&gt;wipes on reboot&lt;/strong&gt;. Some distros also wipe files older than 10 days via &lt;code&gt;systemd-tmpfiles&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whatever apps and users decide to drop there: download fragments, session data, sockets, scratch files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Great for one-off testing, scratch tarballs, anything you don&amp;rsquo;t care about.&lt;/li&gt;
&lt;li&gt;Don&amp;rsquo;t put real files here. They will disappear. I have watched more than one person lose a script they &amp;ldquo;saved real quick&amp;rdquo; to &lt;code&gt;/tmp&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="9-mnt-and-media---mount-points-for-external-drives"&gt;9. &lt;code&gt;/mnt&lt;/code&gt; and &lt;code&gt;/media&lt;/code&gt; - Mount Points for External Drives
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;What they do:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Both directories are mount points for drives that aren&amp;rsquo;t part of the root filesystem: USB sticks, extra hard drives, NFS shares, ISOs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What&amp;rsquo;s inside:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mnt/&lt;/code&gt;: Generic spot for manual mounts. Make a subfolder and mount whatever you want there.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/media/&lt;/code&gt;: Where desktop environments auto-mount removable drives (e.g., &lt;code&gt;/media/yourusername/USB_NAME&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why they matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On a server with no desktop, almost everything you mount lands in &lt;code&gt;/mnt/&lt;/code&gt; because you put it there.&lt;/li&gt;
&lt;li&gt;Plug in a USB drive and nothing shows up in &lt;code&gt;/media/&lt;/code&gt;? You&amp;rsquo;re on a headless box. Mount it by hand.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/usb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /dev/sdc1 /mnt/usb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Use &lt;code&gt;lsblk&lt;/code&gt; first to find the right device name. Don&amp;rsquo;t guess.&lt;/p&gt;
&lt;h2 id="10-dev-proc-and-sys---system-and-hardware-information"&gt;10. &lt;code&gt;/dev&lt;/code&gt;, &lt;code&gt;/proc&lt;/code&gt;, and &lt;code&gt;/sys&lt;/code&gt; - System and Hardware Information
&lt;/h2&gt;&lt;p&gt;These three are virtual filesystems. Nothing in them lives on disk. The kernel builds them on the fly.&lt;/p&gt;
&lt;h3 id="dev---device-files"&gt;&lt;code&gt;/dev/&lt;/code&gt; - Device Files
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Files that represent hardware:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/dev/sda&lt;/code&gt;, &lt;code&gt;/dev/sdb&lt;/code&gt;, etc. for SATA and USB disks&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/nvme0n1&lt;/code&gt; for NVMe drives&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/tty*&lt;/code&gt; for terminals&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/null&lt;/code&gt; and &lt;code&gt;/dev/zero&lt;/code&gt; for the classic Unix tricks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="proc---process-and-kernel-information"&gt;&lt;code&gt;/proc/&lt;/code&gt; - Process and Kernel Information
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Live system info, regenerated by the kernel:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/proc/cpuinfo&lt;/code&gt; for CPU details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc/meminfo&lt;/code&gt; for RAM usage&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/&lt;/code&gt; for everything about a running process&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="sys---kernel-and-hardware-settings"&gt;&lt;code&gt;/sys/&lt;/code&gt; - Kernel and Hardware Settings
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;A structured view of devices, drivers, and kernel knobs you can read and sometimes write to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why they matter:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Need CPU info without installing anything? &lt;code&gt;cat /proc/cpuinfo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Looking for the right disk to format? &lt;code&gt;lsblk&lt;/code&gt; reads from &lt;code&gt;/sys&lt;/code&gt; and tells you exactly what&amp;rsquo;s there.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/&lt;/code&gt; is how you talk to hardware. Mount commands, &lt;code&gt;dd&lt;/code&gt;, &lt;code&gt;fdisk&lt;/code&gt;, every one of them points at &lt;code&gt;/dev/&lt;/code&gt; paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;The Linux file system looks like alphabet soup until you understand what each folder is for. Then it stops being scary. You&amp;rsquo;ll know that logs live in &lt;code&gt;/var/log&lt;/code&gt;, configs live in &lt;code&gt;/etc&lt;/code&gt;, your stuff lives in &lt;code&gt;/home&lt;/code&gt;, and the kernel exposes hardware through &lt;code&gt;/dev&lt;/code&gt;, &lt;code&gt;/proc&lt;/code&gt;, and &lt;code&gt;/sys&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Best way to lock it in? Go look. Open a terminal and poke around:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /etc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls /usr/bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/cpuinfo &lt;span class="p"&gt;|&lt;/span&gt; head -20
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read what&amp;rsquo;s there. The Linux file hierarchy isn&amp;rsquo;t a mystery once you&amp;rsquo;ve seen it. It&amp;rsquo;s a map, and now you&amp;rsquo;ve got the legend.&lt;/p&gt;</description></item><item><title>Master the Basics - How to Install Docker in Linux</title><link>https://diymediaserver.com/post/master-the-basics-how-to-install-docker/</link><pubDate>Thu, 13 Feb 2025 07:02:47 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-how-to-install-docker/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-how-to-install-docker/docker_hu_7cec33cd094d6706.webp" alt="Featured image of post Master the Basics - How to Install Docker in Linux" /&gt;&lt;h2 id="what-is-docker-and-why-should-you-use-it"&gt;What Is Docker and Why Should You Use It?
&lt;/h2&gt;&lt;p&gt;Docker is a containerization platform that lets you run applications inside isolated environments called containers. Each container packages an app with all its dependencies, so it stays lightweight, portable, and easy to manage.&lt;/p&gt;
&lt;h3 id="why-use-docker"&gt;Why Use Docker?
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Simpler deployments.&lt;/strong&gt; Instead of installing software directly on your server and fighting dependency conflicts, Docker bundles everything into a self-contained unit. It runs the same way on every system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lighter on resources.&lt;/strong&gt; Docker containers share the host OS kernel, so they&amp;rsquo;re far cheaper on memory and CPU than a stack of full virtual machines. You can comfortably run dozens of containers on a single box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Easier software management.&lt;/strong&gt; Want Jellyfin, Sonarr, Radarr, or Home Assistant? You pull an image, run a container, and you&amp;rsquo;re done. No hand-compiling, no dependency hell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isolation by default.&lt;/strong&gt; Containers don&amp;rsquo;t see each other. If one crashes or gets compromised, the rest of the system keeps running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Updates and rollbacks are cheap.&lt;/strong&gt; Pulling a newer image is one command. Rolling back to a known-good tag is one more.&lt;/p&gt;
&lt;p&gt;Now that you know why Docker is worth running on your home server, let&amp;rsquo;s install it correctly with security in mind.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0BKQ7KRZ1"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/NUC12_hu_9ee5aa66c06c6439.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/NUC12_hu_1d8c63c0222251f5.webp" width="600" height="377" alt="Intel NUC 12 Pro (NUC12WSHi5)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Intel NUC 12 Pro (NUC12WSHi5)&lt;/strong&gt;
Compact mini PC for lightweight servers, GPU Passthrough, Docker stacks, and VMs.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3JyPlM4" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="1-update-your-system-and-install-dependencies"&gt;1. Update Your System and Install Dependencies
&lt;/h2&gt;&lt;p&gt;Before installing Docker, update your system and pull in the prerequisites. This gives you a clean, stable base.&lt;/p&gt;
&lt;p&gt;Run the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y ca-certificates curl gnupg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refresh the package list (&lt;code&gt;apt update&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Upgrade installed packages (&lt;code&gt;apt upgrade -y&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Install the dependencies you&amp;rsquo;ll need (if &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;gnupg&lt;/code&gt; aren&amp;rsquo;t already on the box)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="2-add-dockers-official-gpg-key-and-repository"&gt;2. Add Docker&amp;rsquo;s Official GPG Key and Repository
&lt;/h2&gt;&lt;p&gt;Ubuntu ships a Docker package in its default repositories, but you shouldn&amp;rsquo;t use it. Here&amp;rsquo;s why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Outdated versions.&lt;/strong&gt; Ubuntu&amp;rsquo;s repo lags behind Docker&amp;rsquo;s latest stable releases, so you miss features and security fixes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slower updates.&lt;/strong&gt; Critical bug fixes and security patches land in Docker&amp;rsquo;s official repo first. Ubuntu&amp;rsquo;s version gets them later, if at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing features.&lt;/strong&gt; The Ubuntu package can lack support for newer Docker functionality.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, install from Docker&amp;rsquo;s official repository. Run the following commands.&lt;/p&gt;
&lt;h3 id="cheat-sheet---cut--paste-commands"&gt;Cheat Sheet - Cut &amp;amp; Paste Commands:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -m &lt;span class="m"&gt;0755&lt;/span&gt; -d /etc/apt/keyrings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://download.docker.com/linux/ubuntu/gpg &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/keyrings/docker.asc &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod a+r /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [arch=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release -cs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="the-long-and-detailed-explanation-of-the-commands"&gt;The Long and Detailed Explanation of the Commands:
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo install -m &lt;span class="m"&gt;0755&lt;/span&gt; -d /etc/apt/keyrings
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="breaking-it-down"&gt;Breaking It Down:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs the command with superuser (root) privileges.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;install&lt;/code&gt; is a command that can copy files, set permissions, and create directories.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-m 0755&lt;/code&gt; sets the directory permissions to &lt;code&gt;0755&lt;/code&gt; (read and execute for everyone, write for the owner).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; tells &lt;code&gt;install&lt;/code&gt; to create a directory if it doesn&amp;rsquo;t already exist.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/apt/keyrings&lt;/code&gt; is where the directory is created. This is where trusted GPG keys for package signing live.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="security-purpose"&gt;Security Purpose:
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Ensures only the owner (root) can modify the keyring directory.&lt;/li&gt;
&lt;li&gt;Prevents unauthorized users from tampering with the trusted GPG keys used to verify software packages.&lt;/li&gt;
&lt;li&gt;Ubuntu 24.04 and later recommend storing GPG keys in &lt;code&gt;/etc/apt/keyrings/&lt;/code&gt; instead of the older &lt;code&gt;/etc/apt/trusted.gpg&lt;/code&gt;. It&amp;rsquo;s better security hygiene.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://download.docker.com/linux/ubuntu/gpg &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/keyrings/docker.asc &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="breaking-the-three-parts-of-this-command-down"&gt;Breaking The Three Parts of This Command Down:
&lt;/h3&gt;&lt;h4 id="1-curl--fssl-httpsdownloaddockercomlinuxubuntugpg"&gt;1. &lt;code&gt;curl -fsSL https://download.docker.com/linux/ubuntu/gpg&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;curl&lt;/code&gt; is a tool for downloading files from the internet.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt; fails silently if the URL is wrong, so you don&amp;rsquo;t end up saving an error page.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt; runs in silent mode (hides progress output).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-S&lt;/code&gt; shows errors when they occur, which makes troubleshooting easier.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-L&lt;/code&gt; follows redirects in case the URL points somewhere else.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://download.docker.com/linux/ubuntu/gpg&lt;/code&gt; is the URL of Docker&amp;rsquo;s official GPG key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What this does: downloads Docker&amp;rsquo;s GPG key, which is used to verify the authenticity of Docker packages.&lt;/p&gt;
&lt;h4 id="2--sudo-tee-etcaptkeyringsdockerasc"&gt;2. &lt;code&gt;| sudo tee /etc/apt/keyrings/docker.asc&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;|&lt;/code&gt; pipes the output of &lt;code&gt;curl&lt;/code&gt; into the &lt;code&gt;tee&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs the command as root (needed to write to system directories).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tee /etc/apt/keyrings/docker.asc&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tee&lt;/code&gt; writes the Docker GPG key into &lt;code&gt;/etc/apt/keyrings/docker.asc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;That file is later used to verify Docker packages are signed and authentic before installing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="3--devnull"&gt;3. &lt;code&gt;&amp;gt; /dev/null&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; redirects output somewhere else.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/null&lt;/code&gt; is the Linux &amp;ldquo;trash&amp;rdquo; file that throws away anything written to it.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod a+r /etc/apt/keyrings/docker.asc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="breaking-it-down-1"&gt;Breaking It Down:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs the command with superuser (root) privileges.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod&lt;/code&gt; changes file permissions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; stands for &amp;ldquo;all users&amp;rdquo; (owner, group, and others).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+r&lt;/code&gt; adds read permission so the file can be read by everyone.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/apt/keyrings/docker.asc&lt;/code&gt; is the GPG key file used to verify Docker packages when installing or updating via &lt;code&gt;apt&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="what-this-does"&gt;What this does:
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Lets the system use the key.&lt;/strong&gt; When &lt;code&gt;apt&lt;/code&gt; installs or updates Docker, it checks package signatures against this GPG key. Without read access, &lt;code&gt;apt&lt;/code&gt; will fail to verify Docker&amp;rsquo;s authenticity and throw errors at you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prevents unauthorized modifications.&lt;/strong&gt; The file is still protected (only root can modify it), but every user can now read it. System processes and normal users can verify packages, but they can&amp;rsquo;t tamper with the key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Follows Ubuntu&amp;rsquo;s newer security guidelines.&lt;/strong&gt; Older versions stored GPG keys in &lt;code&gt;/etc/apt/trusted.gpg&lt;/code&gt;, which gave every key full system-wide trust. That&amp;rsquo;s loose. Newer Ubuntu releases put each key under &lt;code&gt;/etc/apt/keyrings/&lt;/code&gt;, where it&amp;rsquo;s isolated and trusted only for its own repository.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [arch=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dpkg --print-architecture&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release -cs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="breaking-the-three-parts-of-this-command-down-1"&gt;Breaking The Three Parts of This Command Down:
&lt;/h3&gt;&lt;h4 id="1-echo-deb--httpsdownloaddockercomlinuxubuntu-lsb_release--cs-stable"&gt;1. &lt;code&gt;echo &amp;quot;deb [...] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot;&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;echo&lt;/code&gt; prints the text inside the quotes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;deb [...] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot;&lt;/code&gt; is a software repository entry in Debian/Ubuntu format.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="whats-inside-the-deb--line"&gt;What&amp;rsquo;s inside the &lt;code&gt;deb [...]&lt;/code&gt; line?
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deb&lt;/code&gt; tells &lt;code&gt;apt&lt;/code&gt; this is a binary package repository, not source code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc]&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;arch=$(dpkg --print-architecture)&lt;/code&gt; detects your system architecture (&lt;code&gt;amd64&lt;/code&gt;, &lt;code&gt;arm64&lt;/code&gt;, etc.) so you install the right build.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signed-by=/etc/apt/keyrings/docker.asc&lt;/code&gt; uses Docker&amp;rsquo;s GPG key (the one you downloaded a minute ago) to verify package authenticity.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://download.docker.com/linux/ubuntu&lt;/code&gt; is the URL of Docker&amp;rsquo;s official repository.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(lsb_release -cs)&lt;/code&gt; inserts your Ubuntu codename (e.g., &lt;code&gt;noble&lt;/code&gt; for 24.04) so you get the right release.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stable&lt;/code&gt; pulls the stable Docker channel instead of edge or testing builds.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2--sudo-tee-etcaptsourceslistddockerlist"&gt;2. &lt;code&gt;| sudo tee /etc/apt/sources.list.d/docker.list&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;|&lt;/code&gt; pipes the &lt;code&gt;echo&lt;/code&gt; output into &lt;code&gt;tee&lt;/code&gt;, which writes it to a file.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs the command with root privileges, since &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt; requires admin access.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tee /etc/apt/sources.list.d/docker.list&lt;/code&gt; saves the repository entry into &lt;code&gt;/etc/apt/sources.list.d/docker.list&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="3--devnull-1"&gt;3. &lt;code&gt;&amp;gt; /dev/null&lt;/code&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Throws away the output from &lt;code&gt;tee&lt;/code&gt; so your terminal stays clean.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Update your apt repositories:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="3-install-docker"&gt;3. Install Docker
&lt;/h2&gt;&lt;p&gt;Now install Docker. The following command also pulls in the dependencies:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y docker-ce
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make sure Docker starts at boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once installed and enabled, verify Docker is running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If Docker is running, you should see this after running &lt;code&gt;docker ps&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With nothing showing under those headers.&lt;/p&gt;
&lt;h3 id="security-step-prevent-unauthorized-docker-access"&gt;Security Step: Prevent Unauthorized Docker Access
&lt;/h3&gt;&lt;p&gt;By default, Docker runs as root. That&amp;rsquo;s a problem. Anyone in the &lt;code&gt;docker&lt;/code&gt; group gets effective root on the host, so add a dedicated group and put your user in it deliberately:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo groupadd docker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -aG docker &lt;span class="nv"&gt;$USER&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;newgrp docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After running these commands, you can run Docker without &lt;code&gt;sudo&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run hello-world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="4-configure-docker-for-better-security"&gt;4. Configure Docker for Better Security
&lt;/h2&gt;&lt;p&gt;A stock Docker install carries real risk. Let&amp;rsquo;s harden it.&lt;/p&gt;
&lt;h3 id="enable-apparmor"&gt;Enable AppArmor
&lt;/h3&gt;&lt;p&gt;Ubuntu ships with AppArmor, a security module that restricts what Docker containers can do on the host.&lt;/p&gt;
&lt;p&gt;Check whether it&amp;rsquo;s enabled:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo aa-status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it isn&amp;rsquo;t, turn it on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now apparmor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="stop-containers-from-getting-root-privileges"&gt;Stop Containers from Getting Root Privileges
&lt;/h3&gt;&lt;p&gt;By default, containers can run with elevated privileges. Restrict that with user namespaces.&lt;/p&gt;
&lt;h4 id="cheat-sheet---cut--paste-these-commands"&gt;Cheat Sheet - Cut &amp;amp; Paste These Commands:
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /etc/systemd/system/docker.service.d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;[Service]\nExecStart=\nExecStart=/usr/bin/dockerd --userns-remap=default&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/systemd/system/docker.service.d/override.conf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="breakdown-of-these-commands"&gt;Breakdown of These Commands:
&lt;/h4&gt;&lt;p&gt;Create a configuration directory for Docker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /etc/systemd/system/docker.service.d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs the command as root (required for modifying system settings).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkdir -p&lt;/code&gt; creates a directory if it doesn&amp;rsquo;t already exist (&lt;code&gt;-p&lt;/code&gt; keeps it from erroring out if the directory&amp;rsquo;s already there).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/systemd/system/docker.service.d&lt;/code&gt; is where custom systemd overrides for Docker live.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; This gives you a place to drop a custom configuration for the Docker service without editing the main service file.&lt;/p&gt;
&lt;p&gt;Create an override file to enable user namespace remapping:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;[Service]\nExecStart=\nExecStart=/usr/bin/dockerd --userns-remap=default&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/systemd/system/docker.service.d/override.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;echo -e &amp;quot;[Service]\nExecStart=\nExecStart=/usr/bin/dockerd --userns-remap=default&amp;quot;&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;Builds a custom systemd service override for Docker.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart=&lt;/code&gt; (empty) clears the previous &lt;code&gt;ExecStart&lt;/code&gt; from the default Docker service.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart=/usr/bin/dockerd --userns-remap=default&lt;/code&gt; replaces it with one that enables user namespace remapping.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;| sudo tee /etc/systemd/system/docker.service.d/override.conf&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;Pipes the output (&lt;code&gt;|&lt;/code&gt;) into &lt;code&gt;tee&lt;/code&gt;, which writes it into the override file at &lt;code&gt;/etc/systemd/system/docker.service.d/override.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; gives &lt;code&gt;tee&lt;/code&gt; the root access it needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User namespace remapping (&lt;code&gt;--userns-remap=default&lt;/code&gt;) makes Docker containers run as an unprivileged user on the host instead of root.&lt;/li&gt;
&lt;li&gt;If a container is compromised, it doesn&amp;rsquo;t get full root on the host.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reload &lt;code&gt;systemd&lt;/code&gt; to apply the changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs as root.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl daemon-reload&lt;/code&gt; reloads systemd so it picks up the new Docker service override.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Without it, systemd never notices the new &lt;code&gt;override.conf&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Restart Docker to apply the new configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt; runs as root.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl restart docker&lt;/code&gt; restarts Docker so it runs with the new &lt;code&gt;--userns-remap=default&lt;/code&gt; setting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Applies the security change without rebooting.&lt;/li&gt;
&lt;li&gt;From now on, Docker containers run as an unprivileged user instead of root.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping Up
&lt;/h2&gt;&lt;p&gt;Docker is a powerful tool for running apps on your Ubuntu 24.04 server. An insecure setup will bite you. Follow this guide and you&amp;rsquo;ve got Docker installed correctly, with sensible security mitigations in place.&lt;/p&gt;
&lt;p&gt;Want to push security further? Look at &lt;code&gt;ufw&lt;/code&gt; for the firewall layer and &lt;code&gt;fail2ban&lt;/code&gt; for blocking brute-force attempts on exposed services.&lt;/p&gt;
&lt;p&gt;Next up: deploying Sonarr and Radarr on top of this Docker install.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CJGSP9R7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/intel_arc_hu_7fda84478f4d8d55.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/intel_arc_hu_ce1e2b61ba333425.webp" width="600" height="458" alt="ASRock Intel Arc A580 Challenger 8GB OC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;ASRock Intel Arc A580 Challenger 8GB OC&lt;/strong&gt;
The Arc A580 can hold its own in games, but here it’s for obliterating video streams. With support for H.264, HEVC, and full AV1 hardware encode/decode across two media engines, it crushes 20+ 1080p streams or 6–8 HDR tone-mapped 4Ks without breaking a sweat. Drop it in your media server, give Jellyfin direct VA-API access, and watch your CPU finally cool off for a bit.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3TxiGf3" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Master the Basics - How to Install and Use SnapRAID for a Resilient Home Media Server</title><link>https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/</link><pubDate>Sat, 08 Feb 2025 07:26:32 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/snapraid2_hu_8374e90673c22681.webp" alt="Featured image of post Master the Basics - How to Install and Use SnapRAID for a Resilient Home Media Server" /&gt;&lt;h2 id="why-you-need-snapraid-with-mergerfs"&gt;Why You Need SnapRAID with MergerFS
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve spent years building the perfect media collection. Movies, TV shows, music, personal backups, all neatly spread across a stack of hard drives. Then one day you hear that clicking sound coming out of the server. Panic. Did you lose everything on that drive? Not if you planned ahead.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s where MergerFS and SnapRAID come in. MergerFS pools multiple drives into one logical storage location, and SnapRAID adds parity so you can recover from a drive failure. If you haven&amp;rsquo;t set up MergerFS yet, start with my &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/" &gt;Master the Basics - MergerFS&lt;/a&gt;&lt;/strong&gt; post. Together they give you a cheap, flexible way to protect your data without the headaches of traditional RAID.&lt;/p&gt;
&lt;p&gt;SnapRAID is a parity-based protection system. Unlike traditional RAID, it doesn&amp;rsquo;t constantly write to every disk. That makes it a good fit for media servers, where most of your data sits unchanged for months at a time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been running this setup on my own Debian-based media server with a mix of mismatched drives, and it&amp;rsquo;s saved me from at least one dying disk. Here&amp;rsquo;s how to install and configure it.&lt;/p&gt;
&lt;h2 id="what-is-snapraid-parity"&gt;What is SnapRAID Parity?
&lt;/h2&gt;&lt;p&gt;SnapRAID parity is the backbone of the whole thing. It works by creating a file containing mathematical parity data about the files on your data disks. If a drive dies, SnapRAID uses that parity file to reconstruct what was on it.&lt;/p&gt;
&lt;p&gt;Think of parity as an insurance policy for your media collection. If one of your drives crashes, the parity file is the blueprint that lets SnapRAID rebuild the missing data using the remaining disks.&lt;/p&gt;
&lt;h3 id="how-snapraid-parity-works"&gt;How SnapRAID Parity Works
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Parity calculation:&lt;/strong&gt; SnapRAID scans your data disks and calculates parity based on their contents. The result lives on a dedicated parity disk, separate from the data drives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parity updates:&lt;/strong&gt; Parity does not refresh on its own. You run &lt;code&gt;snapraid sync&lt;/code&gt; when you want it updated. I&amp;rsquo;ll cover automating that with cron later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drive recovery:&lt;/strong&gt; When a data disk fails, SnapRAID uses the surviving data drives plus the parity disk to rebuild the lost files. More parity disks means more simultaneous failures you can survive.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="why-snapraid-parity-matters"&gt;Why SnapRAID Parity Matters
&lt;/h3&gt;&lt;h4 id="it-protects-against-drive-failures"&gt;It protects against drive failures
&lt;/h4&gt;&lt;p&gt;Without parity, a dead drive means everything on that disk is gone. With parity, you can recover it.&lt;/p&gt;
&lt;h4 id="it-saves-space-compared-to-full-backups"&gt;It saves space compared to full backups
&lt;/h4&gt;&lt;p&gt;Parity stores difference-based data, not a full copy of every drive. One parity drive can protect several data drives. That&amp;rsquo;s a lot cheaper than mirroring everything.&lt;/p&gt;
&lt;h4 id="it-scales-with-your-storage"&gt;It scales with your storage
&lt;/h4&gt;&lt;p&gt;You can mix and match different-sized disks. No need for identical drives like traditional RAID. Start with one parity drive and add more as your array grows.&lt;/p&gt;
&lt;h4 id="you-can-run-multiple-parity-drives"&gt;You can run multiple parity drives
&lt;/h4&gt;&lt;p&gt;One parity drive recovers from one failed disk. Add more if you want more cushion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 parity drive → recovers from 1 failed disk&lt;/li&gt;
&lt;li&gt;2 parity drives → recovers from 2 failed disks&lt;/li&gt;
&lt;li&gt;3 parity drives → recovers from 3 failed disks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To add more parity drives, update &lt;code&gt;/etc/snapraid.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;parity /mnt/disk4/snapraid.parity
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;parity /mnt/disk5/snapraid.parity2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Parity is the difference between losing a movie collection and shrugging off a dead drive. Set up at least one parity disk before you trust the array with anything you care about.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Not sure how many parity drives your array needs? Enter your drive sizes into the SnapRAID calculator and it generates the parity recommendation, the full &lt;code&gt;snapraid.conf&lt;/code&gt;, and the matching MergerFS fstab line.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/page/tools/snapraid-calculator/"
class="backlink-button"
target="_self"
&gt;
Open the calculator
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-is-snapraid-content"&gt;What Is SnapRAID Content?
&lt;/h2&gt;&lt;p&gt;The SnapRAID content file (&lt;code&gt;snapraid.content&lt;/code&gt;) stores metadata about your files: checksums, timestamps, and the layout information needed for recovery. The parity file rebuilds the actual data. The content file tells SnapRAID what your data looked like at the last sync.&lt;/p&gt;
&lt;p&gt;Every &lt;code&gt;snapraid sync&lt;/code&gt; updates the content file with new metadata. That&amp;rsquo;s how SnapRAID detects changes, catches corruption, and rebuilds lost files when you need it to.&lt;/p&gt;
&lt;h3 id="why-keep-snapraidcontent-on-multiple-drives"&gt;Why Keep &lt;code&gt;snapraid.content&lt;/code&gt; on Multiple Drives?
&lt;/h3&gt;&lt;h4 id="one-copy-is-a-single-point-of-failure"&gt;One copy is a single point of failure
&lt;/h4&gt;&lt;p&gt;If your only copy of &lt;code&gt;snapraid.content&lt;/code&gt; lives on &lt;code&gt;disk1&lt;/code&gt; and &lt;code&gt;disk1&lt;/code&gt; dies, recovery fails. SnapRAID no longer has a record of your data layout, so it can&amp;rsquo;t rebuild anything. Put a copy on &lt;code&gt;disk2&lt;/code&gt; and &lt;code&gt;disk3&lt;/code&gt; and SnapRAID still has what it needs.&lt;/p&gt;
&lt;p&gt;A single copy of the content file undercuts the resilience you installed SnapRAID for. Multiple copies are cheap insurance.&lt;/p&gt;
&lt;h3 id="how-to-store-snapraidcontent-on-multiple-drives"&gt;How to Store &lt;code&gt;snapraid.content&lt;/code&gt; on Multiple Drives
&lt;/h3&gt;&lt;p&gt;In &lt;code&gt;/etc/snapraid.conf&lt;/code&gt;, define multiple content locations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content /mnt/disk1/snapraid.content
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content /mnt/disk2/snapraid.content
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content /mnt/disk3/snapraid.content
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each entry creates a duplicate of the content file on a different disk. SnapRAID updates all of them on every sync, so they stay identical.&lt;/p&gt;
&lt;p&gt;Spread the content file across two or more drives and you&amp;rsquo;ve removed a real failure mode. If disaster hits, you still have a path back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can use the same drives for both &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-is-snapraid-data"&gt;What Is SnapRAID Data?
&lt;/h2&gt;&lt;p&gt;In SnapRAID, &amp;ldquo;data&amp;rdquo; means the actual files on your media server: movies, TV shows, music, backups. Those files live on data drives, which are the physical disks SnapRAID protects with parity.&lt;/p&gt;
&lt;p&gt;Unlike traditional RAID, SnapRAID does not mirror or stripe data across disks. Each file exists in one location, and SnapRAID tracks which files live on which drive.&lt;/p&gt;
&lt;h3 id="why-data-drives-have-to-be-defined-in-snapraidconf"&gt;Why Data Drives Have to Be Defined in &lt;code&gt;snapraid.conf&lt;/code&gt;
&lt;/h3&gt;&lt;h4 id="snapraid-needs-to-know-where-your-files-live"&gt;SnapRAID Needs to Know Where Your Files Live
&lt;/h4&gt;&lt;p&gt;SnapRAID uses parity calculations to recover lost files. If you don&amp;rsquo;t tell it which drives hold data, it can&amp;rsquo;t protect them and can&amp;rsquo;t restore anything.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;/etc/snapraid.conf&lt;/code&gt;, you define each data drive explicitly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d1 /mnt/disk1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d2 /mnt/disk2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d3 /mnt/disk3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each &lt;code&gt;data&lt;/code&gt; entry tells SnapRAID:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which disks hold your files&lt;/li&gt;
&lt;li&gt;Which disks need to be included in parity calculations&lt;/li&gt;
&lt;li&gt;Where to look when checking for data integrity&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="it-lets-snapraid-detect-changes-and-fight-corruption"&gt;It Lets SnapRAID Detect Changes and Fight Corruption
&lt;/h4&gt;&lt;p&gt;SnapRAID doesn&amp;rsquo;t track file changes on its own. You run &lt;code&gt;snapraid sync&lt;/code&gt; to update parity.&lt;/p&gt;
&lt;p&gt;With your data drives listed in &lt;code&gt;snapraid.conf&lt;/code&gt;, SnapRAID knows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which files were added, modified, or deleted&lt;/li&gt;
&lt;li&gt;When to update parity data&lt;/li&gt;
&lt;li&gt;How to detect bit rot (silent file corruption)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skip these entries and SnapRAID has no idea what files exist. It can&amp;rsquo;t protect what it doesn&amp;rsquo;t know about.&lt;/p&gt;
&lt;h4 id="it-makes-recovery-possible"&gt;It Makes Recovery Possible
&lt;/h4&gt;&lt;p&gt;If a drive dies, SnapRAID uses parity to reconstruct the lost files. That only works when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The missing drive was listed in &lt;code&gt;snapraid.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A valid parity file exists&lt;/li&gt;
&lt;li&gt;The remaining data disks are intact&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Forget to list a drive in &lt;code&gt;snapraid.conf&lt;/code&gt; and SnapRAID won&amp;rsquo;t protect it. If the drive fails, those files are gone for good because they were never included in parity calculations.&lt;/p&gt;
&lt;h4 id="it-keeps-storage-expansion-painless"&gt;It Keeps Storage Expansion Painless
&lt;/h4&gt;&lt;p&gt;One of SnapRAID&amp;rsquo;s strengths is drive flexibility. Add a new data disk of any size at any time. Mount the new drive and add it to &lt;code&gt;snapraid.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d4 /mnt/disk4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run &lt;code&gt;snapraid sync&lt;/code&gt; and the new disk is now under parity protection. No rebuild. No reformat. Add the drive.&lt;/p&gt;
&lt;p&gt;Defining data drives in &lt;code&gt;snapraid.conf&lt;/code&gt; is non-negotiable. Without these entries, SnapRAID doesn&amp;rsquo;t know what to protect, can&amp;rsquo;t detect changes, and can&amp;rsquo;t restore lost files.&lt;/p&gt;
&lt;p&gt;If you care about keeping your media safe, list every data drive in &lt;code&gt;snapraid.conf&lt;/code&gt; and run &lt;code&gt;snapraid sync&lt;/code&gt; on a schedule.&lt;/p&gt;
&lt;h2 id="installing-snapraid"&gt;Installing SnapRAID
&lt;/h2&gt;&lt;p&gt;On Debian or Ubuntu, installation is one command. SnapRAID lives in the official package repository:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install snapraid -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To verify SnapRAID is installed, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;snapraid status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Self test...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;No configuration file found at &lt;span class="s1"&gt;&amp;#39;/etc/snapraid.conf&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s expected. You haven&amp;rsquo;t written the config yet.&lt;/p&gt;
&lt;h2 id="configuring-snapraid"&gt;Configuring SnapRAID
&lt;/h2&gt;&lt;h3 id="planning-your-snapraid-setup"&gt;Planning Your SnapRAID Setup
&lt;/h3&gt;&lt;p&gt;Before you touch the config, plan it out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How many data disks?&lt;/strong&gt; SnapRAID stores parity on a separate drive, so you need at least one data disk and one parity disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How much parity?&lt;/strong&gt; One parity disk protects against one drive failure. Two parity disks protect against two. Pick based on how much pain a multi-drive failure would cause you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which drives hold parity and content?&lt;/strong&gt; The parity disk needs to be at least as big as your largest data drive. Spread content files across several data drives so you don&amp;rsquo;t lose the recovery layout if one disk dies.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="creating-the-snapraid-configuration-file"&gt;Creating the SnapRAID Configuration File
&lt;/h3&gt;&lt;p&gt;The main configuration file for SnapRAID is &lt;code&gt;/etc/snapraid.conf&lt;/code&gt;. Open it in a text editor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/snapraid.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A basic configuration for three data disks and one parity disk looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Parity file - stored on the largest disk or a dedicated parity drive&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;parity /mnt/disk3/snapraid.parity
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Content files - store metadata; should be on multiple drives for redundancy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content /mnt/disk1/snapraid.content
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content /mnt/disk2/snapraid.content
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Data drives - these contain actual files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d1 /mnt/disk1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d2 /mnt/disk2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data d3 /mnt/disk3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Disk 1 and disk 2 are used for both &lt;code&gt;content&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt;. That&amp;rsquo;s fine.&lt;/p&gt;
&lt;h3 id="key-points-to-consider"&gt;Key Points to Consider
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parity drive:&lt;/strong&gt; Has to be as large as your biggest data drive, or larger.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content files:&lt;/strong&gt; Belong on multiple drives for redundancy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data drives:&lt;/strong&gt; List every drive that holds files you want protected.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="additional-snapraid-options"&gt;Additional SnapRAID Options
&lt;/h3&gt;&lt;p&gt;For better protection and logging, add:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Define the block size used for parity calculations (default is 256K)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;block_size &lt;span class="m"&gt;256&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Auto-save changes to the content file before syncing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;autosave &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Enable scrub (data integrity check) with a limit on recovered errors&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scrub_percent &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scrub_recover &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save and exit the file (&lt;code&gt;CTRL + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;Enter&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="running-snapraid-for-the-first-time"&gt;Running SnapRAID for the First Time
&lt;/h2&gt;&lt;h3 id="step-1-perform-an-initial-sync"&gt;Step 1: Perform an Initial Sync
&lt;/h3&gt;&lt;p&gt;The first time you run SnapRAID, it has to build the initial parity. Run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid sync
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This takes a long time. Hours on a small array, days on a big one. When it finishes, SnapRAID has recorded parity and you can actually recover from a failure.&lt;/p&gt;
&lt;h3 id="step-2-verify-data-integrity"&gt;Step 2: Verify Data Integrity
&lt;/h3&gt;&lt;p&gt;SnapRAID doesn&amp;rsquo;t detect changes on its own, so check it periodically:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid check
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This compares stored parity against the actual files to catch corruption.&lt;/p&gt;
&lt;h3 id="step-3-scrubbing-for-bit-rot"&gt;Step 3: Scrubbing for Bit Rot
&lt;/h3&gt;&lt;p&gt;Files can rot quietly over time. SnapRAID can scrub a slice of your data to catch and fix errors before they spread:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid scrub -p &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p 10&lt;/code&gt; limits scrubbing to 10% of files, which keeps disk wear reasonable.&lt;/li&gt;
&lt;li&gt;Run scrubs on a schedule so issues get caught early.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-4-recovering-a-lost-drive"&gt;Step 4: Recovering a Lost Drive
&lt;/h3&gt;&lt;p&gt;If a drive fails, don&amp;rsquo;t panic. SnapRAID can rebuild it. Replace the dead drive with a new one and mount it in the same location. Then run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo snapraid fix
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SnapRAID reconstructs the missing data using the parity drive.&lt;/p&gt;
&lt;h3 id="automating-snapraid-maintenance"&gt;Automating SnapRAID Maintenance
&lt;/h3&gt;&lt;p&gt;Schedule syncs and scrubs so you don&amp;rsquo;t have to remember. Open the crontab:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;crontab -e
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0 3 * * * /usr/bin/snapraid sync
0 4 * * * /usr/bin/snapraid scrub -p 10
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Sync runs daily at 3 AM to update parity.&lt;/li&gt;
&lt;li&gt;Scrub runs at 4 AM to look for silent corruption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save and exit (&lt;code&gt;CTRL + X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt;, then &lt;code&gt;Enter&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="why-mergerfs-and-snapraid-make-the-perfect-combo"&gt;Why MergerFS and SnapRAID Make the Perfect Combo
&lt;/h2&gt;&lt;p&gt;Run MergerFS and SnapRAID together and you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple storage management.&lt;/strong&gt; All your drives show up as one mount point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible disk usage.&lt;/strong&gt; Mix and match drives of different sizes without rebuilding anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data redundancy.&lt;/strong&gt; SnapRAID protects against disk failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low resource usage.&lt;/strong&gt; No constant mirroring like traditional RAID.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This setup is built for media servers, where most data is read-heavy and rarely changes. When a drive fails, you replace it and run &lt;code&gt;snapraid fix&lt;/code&gt; instead of restoring from a full backup.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;Set up SnapRAID with MergerFS once and the next dead drive becomes a chore instead of a crisis. Your files stay organized, accessible, and protected, and you don&amp;rsquo;t have to buy matching drives to keep the array honest.&lt;/p&gt;
&lt;p&gt;Now build the config, run that first sync, and stop relying on luck.&lt;/p&gt;</description></item><item><title>Master the Basics - MergerFS the Best Way to Combine Drives for Your Home Media Server</title><link>https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/</link><pubDate>Sun, 02 Feb 2025 07:23:12 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/mergerfs2_hu_5f4472a23a6e3480.webp" alt="Featured image of post Master the Basics - MergerFS the Best Way to Combine Drives for Your Home Media Server" /&gt;&lt;p&gt;What if you could combine every mismatched drive in your server into one pool? If you&amp;rsquo;re like me, you buy hard drives whenever you find the best price per terabyte. Over time, that leaves you with a pile of different-sized disks and a storage layout that&amp;rsquo;s a mess. Media files end up scattered across drives, and tracking down where anything lives turns into a chore. That&amp;rsquo;s what MergerFS fixes.&lt;/p&gt;
&lt;p&gt;MergerFS combines drives into a single mount point without RAID and without the risk of losing everything if one disk dies. It plays nicely with Sonarr, Radarr, and Jellyfin out of the box.&lt;/p&gt;
&lt;p&gt;In this guide I&amp;rsquo;ll explain how MergerFS works, walk through the configuration I run, and show you how to install it on a Linux media server. By the end you&amp;rsquo;ll have one tidy storage pool that uses every gigabyte you bought.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the kind of mismatched mess MergerFS handles without complaint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Seagate BarraCuda 8TB Internal Hard Drive&lt;/li&gt;
&lt;li&gt;Western Digital 14TB Internal Hard Drive&lt;/li&gt;
&lt;li&gt;Toshiba N300 12TB Internal Hard Drive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any combination of capacities works. MergerFS doesn&amp;rsquo;t care if the sizes match. That&amp;rsquo;s the whole point.&lt;/p&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When you&amp;rsquo;re ready to add a fresh disk to the pile, something like the 24TB Barracuda above slots in next to your existing mismatched drives. No reformatting, no array rebuild, no drama. Mount it, add it to the pool, done.&lt;/p&gt;
&lt;h2 id="what-is-mergerfs-and-why-do-you-need-it"&gt;&lt;strong&gt;What is MergerFS and Why Do You Need It?&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;MergerFS is a &lt;strong&gt;union filesystem&lt;/strong&gt; for Linux that combines multiple storage devices into a single mount point. Instead of juggling separate drives, you access everything from one unified path. Compared to RAID, MergerFS is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;/strong&gt; - Add or remove drives anytime. No array rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe&lt;/strong&gt; - If a drive dies, you only lose what was on that drive. The rest of the pool keeps working.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simple&lt;/strong&gt; - Works with standard filesystems like ext4, XFS, and Btrfs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transparent&lt;/strong&gt; - Pull a drive out, plug it into any Linux box, and the files are still there as plain files on a regular filesystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-it-works"&gt;&lt;strong&gt;How It Works&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Think of MergerFS as a smart directory overlay. It doesn&amp;rsquo;t move files or create duplicates. It shows you a virtual filesystem where every drive looks like one.&lt;/p&gt;
&lt;p&gt;For example, if you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/mnt/disk1/Movies/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/disk2/Movies/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/mnt/disk3/Movies/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MergerFS lets you access everything under:&lt;br&gt;
&lt;code&gt;/mnt/storage/Movies/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Apps like Jellyfin and Sonarr only need to look in one place. They don&amp;rsquo;t care which physical disk a file lives on.&lt;/p&gt;
&lt;h2 id="how-to-install-mergerfs-on-your-linux-server"&gt;&lt;strong&gt;How to Install MergerFS on Your Linux Server&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;MergerFS is in the package repos for most Linux distributions. Here&amp;rsquo;s how to install it on Debian-based systems (Ubuntu, Proxmox, etc.) and Arch Linux.&lt;/p&gt;
&lt;h3 id="install-on-ubuntudebian"&gt;&lt;strong&gt;Install on Ubuntu/Debian&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;First, update your package list and install MergerFS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install mergerfs -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The version in your distro&amp;rsquo;s repo is sometimes a release or two behind. For most home media setups that&amp;rsquo;s fine. If you want the latest features, grab the &lt;code&gt;.deb&lt;/code&gt; from the &lt;a class="link" href="https://github.com/trapexit/mergerfs/releases" target="_blank" rel="noopener"
&gt;official MergerFS releases page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="verify-installation"&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Once installed, check the version to confirm it&amp;rsquo;s working:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mergerfs -V
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you get a version string back, you&amp;rsquo;re good. If you get &amp;ldquo;command not found&amp;rdquo;, the package didn&amp;rsquo;t install. Check your repo configuration and try again.&lt;/p&gt;
&lt;h2 id="best-mergerfs-configuration-for-a-home-media-server"&gt;&lt;strong&gt;Best MergerFS Configuration for a Home Media Server&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;Setting it up correctly is what makes the difference between a pool that quietly works and one that fights your media apps. Here&amp;rsquo;s the setup I run.&lt;/p&gt;
&lt;h3 id="step-1-mount-your-drives"&gt;&lt;strong&gt;Step 1: Mount Your Drives&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Before configuring MergerFS, your drives need to be mounted. List what&amp;rsquo;s connected with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsblk -o NAME,FSTYPE,MOUNTPOINT,SIZE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If your drives are &lt;code&gt;/dev/sdb1&lt;/code&gt; and &lt;code&gt;/dev/sdc1&lt;/code&gt;, create mount points:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/disk1 /mnt/disk2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then mount them manually (replace &lt;code&gt;ext4&lt;/code&gt; with your actual filesystem type):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -t ext4 /dev/sdb1 /mnt/disk1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -t ext4 /dev/sdc1 /mnt/disk2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While you&amp;rsquo;re here, grab the UUIDs with &lt;code&gt;sudo blkid&lt;/code&gt; and use those in &lt;code&gt;/etc/fstab&lt;/code&gt; instead of &lt;code&gt;/dev/sdX&lt;/code&gt; paths. Drive letters can shuffle around between reboots. UUIDs don&amp;rsquo;t.&lt;/p&gt;
&lt;h3 id="step-2-create-a-mergerfs-pool"&gt;&lt;strong&gt;Step 2: Create a MergerFS Pool&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Make a directory for the pooled mount:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then mount the drives through MergerFS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mergerfs -o defaults,allow_other,use_ino,cache.files&lt;span class="o"&gt;=&lt;/span&gt;off,dropcacheonclose&lt;span class="o"&gt;=&lt;/span&gt;true,minfreespace&lt;span class="o"&gt;=&lt;/span&gt;50G,category.create&lt;span class="o"&gt;=&lt;/span&gt;mfs /mnt/disk* /mnt/storage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="understanding-the-options"&gt;&lt;strong&gt;Understanding the Options&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Here&amp;rsquo;s what each option does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allow_other&lt;/code&gt; - Lets other users (Jellyfin, Sonarr, your Docker containers) access the files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use_ino&lt;/code&gt; - Makes MergerFS report consistent inode numbers across the pool, which keeps hardlinks and rsync happy.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cache.files=off&lt;/code&gt; - Disables FUSE page caching. Better for large media files and avoids stale-cache surprises.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dropcacheonclose=true&lt;/code&gt; - Drops cached pages when a file handle closes. Pair this with &lt;code&gt;cache.files=off&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;category.create=mfs&lt;/code&gt; - New files land on the drive with the most free space.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;minfreespace=50G&lt;/code&gt; - Stops writing to a disk once it has less than 50GB free. Leaves headroom for the filesystem.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-3-make-the-mergerfs-mount-permanent"&gt;&lt;strong&gt;Step 3: Make the MergerFS Mount Permanent&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Edit &lt;code&gt;/etc/fstab&lt;/code&gt; so the pool comes back on every boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add this line at the bottom (adjust paths as needed):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/mnt/disk* /media/Storage fuse.mergerfs direct_io,defaults,allow_other,noforget,dropcacheonclose=true,category.create=mfs,minfreespace=50G,fsname=storage 0 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Save and exit (&lt;code&gt;CTRL+X&lt;/code&gt;, then &lt;code&gt;Y&lt;/code&gt; and &lt;code&gt;ENTER&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Reload the fstab so the new entry takes effect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Confirm it mounted with &lt;code&gt;mount | grep mergerfs&lt;/code&gt; or &lt;code&gt;df -h /media/Storage&lt;/code&gt;. If you see the pool listed and the size adds up to your combined drives, you&amp;rsquo;re done.&lt;/p&gt;
&lt;h2 id="optimizing-mergerfs-for-media-servers"&gt;&lt;strong&gt;Optimizing MergerFS for Media Servers&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;Now that the pool is running, here&amp;rsquo;s how to tune it for media work.&lt;/p&gt;
&lt;h3 id="best"&gt;&lt;strong&gt;Best &lt;code&gt;category.create&lt;/code&gt; Policy&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;category.create&lt;/code&gt; option controls where new files land.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mfs&lt;/code&gt; (Most Free Space)&lt;/strong&gt; - Best for spreading files across drives evenly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;epmfs&lt;/code&gt; (Existing Path, Most Free Space)&lt;/strong&gt; - Keeps related files together. Good if you want a whole TV show season on the same disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lus&lt;/code&gt; (Least Used Space)&lt;/strong&gt; - Fills one drive before moving to the next. Useful if you want to spin down idle disks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most media servers, &lt;code&gt;mfs&lt;/code&gt; is the right call. It balances usage and keeps any single drive from filling up first.&lt;/p&gt;
&lt;h3 id="handling-deleted-files"&gt;&lt;strong&gt;Handling Deleted Files&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;When you delete a file from the pool, MergerFS removes it from the drive that held it. If a deleted file looks like it&amp;rsquo;s still hanging around, drop the filesystem cache:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sync &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /proc/sys/vm/drop_caches
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That clears the kernel&amp;rsquo;s page cache and forces the next directory listing to read fresh from disk.&lt;/p&gt;
&lt;h2 id="why-mergerfs-is-perfect-for-home-media-servers"&gt;&lt;strong&gt;Why MergerFS is Perfect for Home Media Servers&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;MergerFS is the right call for combining mismatched drives into one storage pool. It&amp;rsquo;s flexible, quick to set up, and plays nicely with Jellyfin, Sonarr, and Radarr.&lt;/p&gt;
&lt;h3 id="key-benefits"&gt;&lt;strong&gt;Key Benefits:&lt;/strong&gt;
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Easy Setup&lt;/strong&gt; - No RAID configuration. No array rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Total Data Loss&lt;/strong&gt; - Each drive is a regular filesystem. Lose one and you only lose what was on it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible Expansion&lt;/strong&gt; - Add or pull drives whenever you need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built for Media Servers&lt;/strong&gt; - Works with Jellyfin, Proxmox, and Docker without fighting them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tired of juggling individual disks? Install MergerFS, set it up the way I showed above, and get on with watching your media.&lt;/p&gt;</description></item><item><title>OpenMediaVault vs TrueNAS vs Unraid: Best NAS OS for Homelabs</title><link>https://diymediaserver.com/post/choosing-the-right-nas-solution-openmediavault-vs-unraid-vs-truenas-core-vs-truenas-scale-vs-diy/</link><pubDate>Fri, 31 Jan 2025 07:24:52 -0700</pubDate><guid>https://diymediaserver.com/post/choosing-the-right-nas-solution-openmediavault-vs-unraid-vs-truenas-core-vs-truenas-scale-vs-diy/</guid><description>&lt;img src="https://diymediaserver.com/post/choosing-the-right-nas-solution-openmediavault-vs-unraid-vs-truenas-core-vs-truenas-scale-vs-diy/softwarenas2_hu_e066e66a0c3282d1.webp" alt="Featured image of post OpenMediaVault vs TrueNAS vs Unraid: Best NAS OS for Homelabs" /&gt;&lt;p&gt;Picking a NAS OS sounds easy until you actually try. Each one swears it&amp;rsquo;s the right call. Each one breaks differently when you push it. So before you commit a stack of drives and a weekend to the wrong platform, here&amp;rsquo;s what each option costs you, what it handles well, and where it falls apart.&lt;/p&gt;
&lt;p&gt;This post compares the five NAS options I&amp;rsquo;ve run or seriously evaluated: OpenMediaVault, Unraid, TrueNAS Core, TrueNAS Scale, and rolling your own. I&amp;rsquo;ll lay out the honest pros and cons of each so you can make the call without sitting through marketing pages.&lt;/p&gt;
&lt;h2 id="1-openmediavault-omv"&gt;1. OpenMediaVault (OMV)
&lt;/h2&gt;&lt;p&gt;&lt;a href="https://www.openmediavault.org/" target="_blank" rel="noopener"&gt;&lt;b&gt;OpenMediaVault&lt;/b&gt;&lt;/a&gt; (OMV) is a Debian-based NAS OS that turns standard hardware into a web-managed server. The GUI handles storage, users, and shares without dropping you straight into the terminal, which makes the on-ramp easier for folks who don&amp;rsquo;t live in a shell. It speaks SMB, NFS, and FTP out of the box, so anything on your network can mount it. RAID management, scheduled backups, and basic monitoring are all wired into the same interface.&lt;/p&gt;
&lt;p&gt;The real strength is the plugin system. Docker, rsync, and a stack of community plugins extend OMV well past the default feature set. The catch: OMV is happiest when you stay close to the official release path. Heavy customization tends to bite you on the next update.&lt;/p&gt;
&lt;h3 id="pros"&gt;Pros
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Free and open source. No licensing fees.&lt;/li&gt;
&lt;li&gt;Debian-based, so any Debian package is fair game.&lt;/li&gt;
&lt;li&gt;Web GUI for setup and day-to-day management.&lt;/li&gt;
&lt;li&gt;Plugin system for Docker, rsync, and more.&lt;/li&gt;
&lt;li&gt;Supports EXT4, XFS, and Btrfs.&lt;/li&gt;
&lt;li&gt;Docker and virtualization support for extra services.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons"&gt;Cons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Light on enterprise features.&lt;/li&gt;
&lt;li&gt;Heavy customization causes stability issues. Plugins also fight each other.&lt;/li&gt;
&lt;li&gt;Support is community forums only.&lt;/li&gt;
&lt;li&gt;Interface and updates are rougher than the polished competition.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="notes"&gt;Notes
&lt;/h3&gt;&lt;p&gt;I ran OMV for several years on my own gear. It handles the default workload fine. The moment you start stacking custom plugins, third-party repos, or anything outside the official update path, stability gets shaky. If you want something to install once and leave alone, it works. If you want to tinker, you&amp;rsquo;ll be fighting it.&lt;/p&gt;
&lt;h2 id="2-unraid"&gt;2. Unraid
&lt;/h2&gt;&lt;p&gt;&lt;a href="https://unraid.net/" target="_blank" rel="noopener"&gt;&lt;b&gt;Unraid&lt;/b&gt;&lt;/a&gt; bundles NAS, container hosting, and virtualization into one OS. It boots off a USB stick and runs in RAM, so the system disk isn&amp;rsquo;t part of your storage pool. A modern Linux kernel underneath means it&amp;rsquo;ll run on pretty much any 64-bit x86 box, from a recycled office tower to a proper homelab build.&lt;/p&gt;
&lt;p&gt;The web UI is the part most people fall in love with. Sensible defaults out of the box, with knobs to turn when you want them. The three jobs Unraid does. Network storage, Docker, and VMs. They all live behind the same interface, so you&amp;rsquo;re not bouncing between five tools to spin up a media server or a personal cloud.&lt;/p&gt;
&lt;h3 id="pros-1"&gt;Pros
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Flexible storage pooling. Mix and match drives of different sizes.&lt;/li&gt;
&lt;li&gt;Parity-based protection instead of traditional RAID. Drive replacement and expansion are painless.&lt;/li&gt;
&lt;li&gt;Strong Docker and VM support. Jellyfin, Sonarr, Radarr, full Linux VMs.&lt;/li&gt;
&lt;li&gt;Web interface is genuinely friendly.&lt;/li&gt;
&lt;li&gt;Efficient storage usage with one or two parity disks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-1"&gt;Cons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Paid license. $49 to $109 depending on drive count.&lt;/li&gt;
&lt;li&gt;Subscription model now, or pony up $249 for the Lifetime License.&lt;/li&gt;
&lt;li&gt;Light on enterprise features.&lt;/li&gt;
&lt;li&gt;Single-disk-at-a-time writes are slower than RAID. A cache disk fixes most of this.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="notes-1"&gt;Notes
&lt;/h3&gt;&lt;p&gt;The price has always kept me off Unraid. When they moved to a subscription model, that closed the door for me entirely. Plenty of people swear by it, and I get why. The Docker and VM experience is hard to beat. If the licensing doesn&amp;rsquo;t bother you, grab the 30-day free trial and see how it feels on your hardware before you commit.&lt;/p&gt;
&lt;h2 id="3-truenas-core"&gt;3. TrueNAS Core
&lt;/h2&gt;&lt;p&gt;&lt;a href="https://www.truenas.com/truenas-core/" target="_blank" rel="noopener"&gt;&lt;b&gt;TrueNAS Core&lt;/b&gt;&lt;/a&gt; is the FreeBSD-based, ZFS-backed NAS OS formerly known as FreeNAS. ZFS is the headline feature. Snapshots, checksums, self-healing, the works. If data integrity is the top priority, this is the platform built around it. SMB, NFS, iSCSI, and AFP cover almost any client you&amp;rsquo;ll throw at it.&lt;/p&gt;
&lt;p&gt;The web UI is dense but powerful. Plugins and jails let you run Jellyfin, Sonarr, Radarr, or backup tooling on the NAS itself instead of standing up another machine. The trade-off is hardware. ZFS wants ECC RAM and plenty of memory. Skimp on either and you give up some of the data integrity story you came here for.&lt;/p&gt;
&lt;h3 id="pros-2"&gt;Pros
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;ZFS. Data integrity, snapshots, compression.&lt;/li&gt;
&lt;li&gt;Enterprise protocols. iSCSI, SMB, NFS.&lt;/li&gt;
&lt;li&gt;Web-based management with deep configuration options.&lt;/li&gt;
&lt;li&gt;Free and open source.&lt;/li&gt;
&lt;li&gt;VMs and jails for running services on the NAS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-2"&gt;Cons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Hardware hungry. ECC RAM and serious memory are expected.&lt;/li&gt;
&lt;li&gt;Adding drives to a ZFS pool one at a time is inefficient.&lt;/li&gt;
&lt;li&gt;FreeBSD is less familiar than Linux for most homelab users.&lt;/li&gt;
&lt;li&gt;ZFS and FreeBSD both have a learning curve.&lt;/li&gt;
&lt;li&gt;Storage expansion needs planning up front.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="notes-2"&gt;Notes
&lt;/h3&gt;&lt;p&gt;If I could buy all my storage up front and had a board with ECC support, TrueNAS Core or Scale would be the easy call. Buying six to eight large drives at once isn&amp;rsquo;t realistic for me. Even with the budget, I&amp;rsquo;d pick Scale over Core. The Linux base, container support, and broader hardware compatibility make Scale a better fit for the way I use a NAS.&lt;/p&gt;
&lt;h2 id="4-truenas-scale"&gt;4. TrueNAS Scale
&lt;/h2&gt;&lt;p&gt;&lt;a href="https://www.truenas.com/truenas-scale/" target="_blank" rel="noopener"&gt;&lt;b&gt;TrueNAS Scale&lt;/b&gt;&lt;/a&gt; is the Debian Linux sibling to TrueNAS Core. Same ZFS underneath. Same data protection story. The difference is what&amp;rsquo;s bolted on top. Native Kubernetes, Docker, and KVM support, plus the broader Linux hardware compatibility you&amp;rsquo;d expect. If you want a NAS that doubles as an app server or a lightweight hypervisor, Scale is built for that.&lt;/p&gt;
&lt;p&gt;The web UI is similar to Core but designed around the Linux stack. Storage, networking, and apps all live in the same interface. Scale is also built to grow, with clustering and high-availability options available if you ever need them.&lt;/p&gt;
&lt;h3 id="pros-3"&gt;Pros
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Linux-based. Better hardware and software compatibility.&lt;/li&gt;
&lt;li&gt;ZFS for data protection and redundancy.&lt;/li&gt;
&lt;li&gt;Native Kubernetes and Docker support.&lt;/li&gt;
&lt;li&gt;Designed to scale from one box to a cluster.&lt;/li&gt;
&lt;li&gt;Strong fit for homelab and small-business use.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-3"&gt;Cons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Newer than Core. Some features still maturing.&lt;/li&gt;
&lt;li&gt;Same hardware demands as Core. ECC RAM, strong CPU and memory.&lt;/li&gt;
&lt;li&gt;ZFS pool expansion still painful.&lt;/li&gt;
&lt;li&gt;Steeper learning curve than OMV or Unraid.&lt;/li&gt;
&lt;li&gt;ZFS limits on adding drives to existing pools remain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="notes-3"&gt;Notes
&lt;/h3&gt;&lt;p&gt;Same story as Core. If buying the full storage stack up front were on the table and I had ECC-capable hardware, I&amp;rsquo;d run Scale today. The Linux base and container story make it the better pick over Core for the way I work. The upfront cost is what keeps me on a DIY setup instead.&lt;/p&gt;
&lt;h2 id="5-build-your-own-nas-diy"&gt;5. Build Your Own NAS (DIY)
&lt;/h2&gt;&lt;p&gt;A DIY NAS is the option for folks who want full control. Pick the OS. Pick the storage stack. Pick the filesystem. Ubuntu Server, Debian, Arch, even a custom FreeBSD install. Nothing is off the table. That freedom means you can tune the box for whatever you care about. Power draw, transcoding, snapshots, raw capacity. Roll your own MergerFS pool. Roll a ZFS array. Run Btrfs if you like living dangerously. The result is a NAS shaped exactly to your workload instead of someone else&amp;rsquo;s idea of what a NAS should be.&lt;/p&gt;
&lt;p&gt;The cost is complexity. There&amp;rsquo;s no friendly web UI by default. You&amp;rsquo;ll be in a terminal for most setup and maintenance. You&amp;rsquo;re the support team. You&amp;rsquo;re the upgrade path. If you&amp;rsquo;re comfortable with Linux, networking, and storage, that&amp;rsquo;s a fair trade for the flexibility and the lack of licensing fees. If you&amp;rsquo;re not, one of the turnkey options above will save you a lot of weekends.&lt;/p&gt;
&lt;h3 id="pros-4"&gt;Pros
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Fully customizable. Any OS, any hardware, any stack.&lt;/li&gt;
&lt;li&gt;No licensing fees with open-source tooling.&lt;/li&gt;
&lt;li&gt;Total control over RAID, MergerFS, ZFS, or Btrfs.&lt;/li&gt;
&lt;li&gt;Best fit for users comfortable with Linux and storage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cons-4"&gt;Cons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Steep learning curve. Real technical depth required.&lt;/li&gt;
&lt;li&gt;No web UI unless you bolt one on. Webmin, OMV, or Cockpit are the usual options.&lt;/li&gt;
&lt;li&gt;More maintenance. Manual updates, patches, troubleshooting.&lt;/li&gt;
&lt;li&gt;No official support. Community forums and your own notes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="notes-4"&gt;Notes
&lt;/h3&gt;&lt;p&gt;This is the option I chose for my media server NAS. Redundancy wasn&amp;rsquo;t my priority. Capacity was. So I watch SMART data closely and pull disks at the first sign of bad blocks. A catastrophic drive failure means re-ripping a chunk of my movie collection, which is annoying but survivable.&lt;/p&gt;
&lt;p&gt;Critical data is a different story. Family photos and important documents are backed up locally and to the cloud. Redundancy where it matters, raw capacity everywhere else. That&amp;rsquo;s the balance that works for my workload.&lt;/p&gt;
&lt;h2 id="conclusion-which-nas-solution-wins"&gt;Conclusion: Which NAS Solution Wins?
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Beginners and home users:&lt;/strong&gt; OpenMediaVault or Unraid. Easy setup, sane defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media enthusiasts and virtualization:&lt;/strong&gt; Unraid. Docker and VM support are the strongest of the bunch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise and data integrity:&lt;/strong&gt; TrueNAS Core. ZFS, redundancy, and serious uptime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced users and tinkerers:&lt;/strong&gt; DIY. Maximum control if you&amp;rsquo;re willing to do the work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best balance of features and flexibility:&lt;/strong&gt; TrueNAS Scale. ZFS plus Linux plus containers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick the one that matches the gear you have and the time you&amp;rsquo;re willing to spend. If you&amp;rsquo;ve got ECC RAM and a full drive set ready to go, run Scale. If you want Docker and VMs and don&amp;rsquo;t mind the license, run Unraid. If you want free and easy, run OMV. If you want to build the thing yourself, build it. There&amp;rsquo;s no single right answer. Only the one that fits your stack.&lt;/p&gt;</description></item><item><title>Master the Basics - The 10 Most Important Linux Commands and How to Use Them</title><link>https://diymediaserver.com/post/master-the-basics-the-10-most-important-linux-commands-and-how-to-use-them/</link><pubDate>Sun, 26 Jan 2025 07:34:05 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-the-10-most-important-linux-commands-and-how-to-use-them/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-the-10-most-important-linux-commands-and-how-to-use-them/linux_commands2_hu_ed559d53c017555a.webp" alt="Featured image of post Master the Basics - The 10 Most Important Linux Commands and How to Use Them" /&gt;&lt;p&gt;You&amp;rsquo;ve installed Linux on your new media server and you&amp;rsquo;re staring at a prompt like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;john@mediaserver:~$&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A blinking cursor and no idea what to type. If you&amp;rsquo;re new to the terminal, that little prompt feels like a black box. Cryptic commands, weird symbols, infinite options. It&amp;rsquo;s intimidating.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the good news. You don&amp;rsquo;t need to know hundreds of commands. You need ten. The ten in this post are the ones I use every single day on Debian and Ubuntu boxes to keep media servers running. Learn these and you&amp;rsquo;ll have the confidence to navigate, manage, and unscrew almost any Linux system you touch.&lt;/p&gt;
&lt;p&gt;Think of it like learning a new language. You don&amp;rsquo;t need the whole dictionary to order a beer. A handful of solid verbs gets you a long way. Same idea here.&lt;/p&gt;
&lt;p&gt;I run these commands daily on Debian 12 and Ubuntu 24.04 servers hosting Plex, Jellyfin, and the *arr stack. Everything below works the same on both.&lt;/p&gt;
&lt;h2 id="1-ls---list-directory-contents"&gt;1. &lt;code&gt;ls&lt;/code&gt; - List Directory Contents
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;ls&lt;/code&gt; is the first command anyone should learn. It shows you what&amp;rsquo;s in a directory. That&amp;rsquo;s it. The Linux equivalent of opening a folder in a file manager, except faster.&lt;/p&gt;
&lt;h3 id="basic-usage"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;Type &lt;code&gt;ls&lt;/code&gt; and it prints the files and folders in your current working directory.&lt;/p&gt;
&lt;h3 id="useful-options"&gt;Useful Options
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ls -l&lt;/code&gt;&lt;/strong&gt;: Detailed view. Permissions, owner, group, size, last modified date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ls -a&lt;/code&gt;&lt;/strong&gt;: Shows hidden files and directories (the ones starting with a dot, like &lt;code&gt;.config&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ls -h&lt;/code&gt;&lt;/strong&gt;: Human-readable file sizes (KB, MB, GB) when paired with &lt;code&gt;-l&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-examples"&gt;Practical Examples
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;See what&amp;rsquo;s in a directory:&lt;/strong&gt;
Running &lt;code&gt;ls /media/Movies&lt;/code&gt; shows every file in that folder. Useful when you can&amp;rsquo;t remember if you already imported that movie.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spot hidden config files:&lt;/strong&gt;
Most app config lives in dotfiles. Use &lt;code&gt;ls -a&lt;/code&gt; when you&amp;rsquo;re troubleshooting or trying to find where some daemon hid its settings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check file details:&lt;/strong&gt;
When permissions or size matter (is this script executable? Did this download finish?), &lt;code&gt;ls -lh&lt;/code&gt; gives you everything in one shot.&lt;/p&gt;
&lt;h3 id="why-it-matters"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;You can&amp;rsquo;t manage what you can&amp;rsquo;t see. &lt;code&gt;ls&lt;/code&gt; is how you see. Get comfortable with it first, because every other command in this list assumes you know what&amp;rsquo;s on disk.&lt;/p&gt;
&lt;h2 id="2-cd---change-directory"&gt;2. &lt;code&gt;cd&lt;/code&gt; - Change Directory
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cd&lt;/code&gt; is how you move around. It stands for &amp;ldquo;change directory,&amp;rdquo; which is exactly what it does. Think of it as double-clicking a folder, except your hands never leave the keyboard.&lt;/p&gt;
&lt;h3 id="basic-usage-1"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;cd &amp;lt;directory&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Move into that directory.&lt;/p&gt;
&lt;p&gt;Want to jump to your Movies folder?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /media/Movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="useful-shortcuts"&gt;Useful Shortcuts
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Home directory:&lt;/strong&gt; &lt;code&gt;cd&lt;/code&gt; on its own takes you back home, no matter where you are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parent directory:&lt;/strong&gt; &lt;code&gt;cd ..&lt;/code&gt; moves you one level up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Root directory:&lt;/strong&gt; &lt;code&gt;cd /&lt;/code&gt; drops you at the top of the file system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Previous directory:&lt;/strong&gt; &lt;code&gt;cd -&lt;/code&gt; jumps back to wherever you were before the last &lt;code&gt;cd&lt;/code&gt;. This one&amp;rsquo;s a lifesaver when you&amp;rsquo;re bouncing between two folders.&lt;/p&gt;
&lt;h3 id="practical-examples-1"&gt;Practical Examples
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Jump to an absolute path:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Jump straight to a nested folder, no need to walk down step by step:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /media/Shows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Back up one level:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="why-it-matters-1"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;ll run &lt;code&gt;cd&lt;/code&gt; more than any other command. Efficient navigation is the difference between feeling lost and feeling in control. Learn it cold.&lt;/p&gt;
&lt;h3 id="troubleshooting"&gt;Troubleshooting
&lt;/h3&gt;&lt;p&gt;If &lt;code&gt;cd&lt;/code&gt; errors out, check the directory name. Linux file systems are &lt;strong&gt;case-sensitive&lt;/strong&gt;. &lt;code&gt;media&lt;/code&gt; and &lt;code&gt;Media&lt;/code&gt; are two different folders. Run &lt;code&gt;ls&lt;/code&gt; first to confirm what&amp;rsquo;s there.&lt;/p&gt;
&lt;h2 id="3-pwd---print-working-directory"&gt;3. &lt;code&gt;pwd&lt;/code&gt; - Print Working Directory
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;pwd&lt;/code&gt; stands for &amp;ldquo;print working directory.&amp;rdquo; It tells you exactly where you are in the file system. Think of it as the &amp;ldquo;you are here&amp;rdquo; arrow on a mall map.&lt;/p&gt;
&lt;h3 id="basic-usage-2"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;Type &lt;code&gt;pwd&lt;/code&gt; and you get the full absolute path of your current directory.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re in your Movies folder, &lt;code&gt;pwd&lt;/code&gt; returns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media/Movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="practical-use-cases"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Know where you are:&lt;/strong&gt;
After three SSH sessions and ten &lt;code&gt;cd&lt;/code&gt; commands, it&amp;rsquo;s easy to lose track. &lt;code&gt;pwd&lt;/code&gt; ends the guessing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Script writing:&lt;/strong&gt;
When you write a shell script that builds relative paths, &lt;code&gt;pwd&lt;/code&gt; confirms you&amp;rsquo;re starting from the right place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt;
A command not working? Half the time you&amp;rsquo;re in the wrong directory. &lt;code&gt;pwd&lt;/code&gt; tells you in one second.&lt;/p&gt;
&lt;h3 id="why-it-matters-2"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s a small command, but it&amp;rsquo;s the one you reach for when you&amp;rsquo;re confused. Pair it with &lt;code&gt;ls&lt;/code&gt; and &lt;code&gt;cd&lt;/code&gt; and you can always orient yourself.&lt;/p&gt;
&lt;h3 id="example-workflow"&gt;Example Workflow
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;You SSH into a remote server and don&amp;rsquo;t know where you landed. Run &lt;code&gt;pwd&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You&amp;rsquo;ve changed directories five times. Run &lt;code&gt;pwd&lt;/code&gt; again before you copy or delete anything.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="tips"&gt;Tips
&lt;/h3&gt;&lt;p&gt;You can drop it into scripts to log the current directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;You are currently in &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Useful when creating symbolic links or setting environment variables. If you&amp;rsquo;re not sure where you are, you can&amp;rsquo;t be sure where the link will point.&lt;/p&gt;
&lt;h2 id="4-mkdir---make-directory"&gt;4. &lt;code&gt;mkdir&lt;/code&gt; - Make Directory
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;mkdir&lt;/code&gt; creates new directories. Whether you&amp;rsquo;re laying out your media library or staging a backup folder, this is your tool.&lt;/p&gt;
&lt;h3 id="basic-usage-3"&gt;Basic Usage
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir &amp;lt;directory-name&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates a directory in your current location.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That creates a folder called &lt;code&gt;media&lt;/code&gt; right where you are.&lt;/p&gt;
&lt;h3 id="practical-use-cases-1"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Build a media library structure in one command:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /media/&lt;span class="o"&gt;{&lt;/span&gt;Movies,Shows,Music&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That builds the &lt;code&gt;media&lt;/code&gt; folder (if it doesn&amp;rsquo;t already exist) and creates Movies, Shows, and Music inside it. One line, three folders, no clicking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spin up a temp folder for scratch work:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir temp_files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="useful-options-1"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-p&lt;/code&gt; (Parent):&lt;/strong&gt; Creates parent directories as needed. Saves you from &amp;ldquo;no such file or directory&amp;rdquo; errors when the path doesn&amp;rsquo;t exist yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v&lt;/code&gt; (Verbose):&lt;/strong&gt; Confirms each directory as it&amp;rsquo;s created. Handy when you&amp;rsquo;re building deep trees.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -pv /media/&lt;span class="o"&gt;{&lt;/span&gt;Movies,Shows,Music&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir: created directory &lt;span class="s1"&gt;&amp;#39;media&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir: created directory &lt;span class="s1"&gt;&amp;#39;media/Movies&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir: created directory &lt;span class="s1"&gt;&amp;#39;media/Shows&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir: created directory &lt;span class="s1"&gt;&amp;#39;media/Music&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="troubleshooting-1"&gt;Troubleshooting
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&amp;ldquo;Permission denied&amp;rdquo;? You don&amp;rsquo;t have write access where you&amp;rsquo;re trying to create the folder. Use &lt;code&gt;sudo mkdir&lt;/code&gt; for system locations like &lt;code&gt;/etc&lt;/code&gt; or &lt;code&gt;/var&lt;/code&gt;. But think before you sudo.&lt;/li&gt;
&lt;li&gt;Typos. Linux is &lt;strong&gt;case-sensitive&lt;/strong&gt;, so &lt;code&gt;media&lt;/code&gt; and &lt;code&gt;Media&lt;/code&gt; are not the same folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-it-matters-3"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;ll create directories constantly. Setting up a Plex library, staging downloads, organizing backups. &lt;code&gt;mkdir -p&lt;/code&gt; is one of those small tools you&amp;rsquo;ll use hundreds of times and never think about. Until you forget the &lt;code&gt;-p&lt;/code&gt; flag and curse at &amp;ldquo;No such file or directory&amp;rdquo; for the tenth time.&lt;/p&gt;
&lt;h2 id="5-rm---remove-files-or-directories"&gt;5. &lt;code&gt;rm&lt;/code&gt; - Remove Files or Directories
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; deletes files and directories. It stands for &amp;ldquo;remove.&amp;rdquo; It does not move things to a trash can. It does not ask &amp;ldquo;are you sure?&amp;rdquo; by default. Files go away. Forever.&lt;/p&gt;
&lt;p&gt;Read that paragraph twice before you run &lt;code&gt;rm&lt;/code&gt; on anything you care about.&lt;/p&gt;
&lt;h3 id="basic-usage-4"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;To remove a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm &amp;lt;file-name&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm old_document.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Gone.&lt;/p&gt;
&lt;h3 id="deleting-multiple-files"&gt;Deleting Multiple Files
&lt;/h3&gt;&lt;p&gt;You can list several files in one command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm file1.txt file2.txt file3.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="removing-directories"&gt;Removing Directories
&lt;/h3&gt;&lt;p&gt;By default, &lt;code&gt;rm&lt;/code&gt; won&amp;rsquo;t touch directories. You need flags.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;rm -r&lt;/code&gt; (Recursive):&lt;/strong&gt; Removes a directory and everything in it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -r folder_name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;rm -rf&lt;/code&gt; (Recursive + Force):&lt;/strong&gt; Deletes everything, no prompts, no warnings.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf /path/to/directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one earned the nickname &amp;ldquo;the nuclear option&amp;rdquo; for a reason. I&amp;rsquo;ve watched people run &lt;code&gt;rm -rf&lt;/code&gt; on &lt;code&gt;/&lt;/code&gt; because of a stray space. Once it&amp;rsquo;s running, you can&amp;rsquo;t get it back. Be deliberate.&lt;/p&gt;
&lt;h3 id="practical-use-cases-2"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Clean up after testing:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -r /tmp/my_temp_files
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Drop a movie you&amp;rsquo;ll never watch again:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -r /media/Movie/Movie_I_No_Longer_Need
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="useful-options-2"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-i&lt;/code&gt; (Interactive):&lt;/strong&gt; Asks before deleting each file. Slow, but it&amp;rsquo;ll save you the day you mistype a path.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -i important_file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v&lt;/code&gt; (Verbose):&lt;/strong&gt; Prints what it&amp;rsquo;s deleting.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rv /path/to/directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-f&lt;/code&gt; (Force):&lt;/strong&gt; Ignores warnings and deletes without asking. Combine with &lt;code&gt;-r&lt;/code&gt; only when you&amp;rsquo;re certain.&lt;/p&gt;
&lt;h3 id="safety-tips"&gt;Safety Tips
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Double-check the path.&lt;/strong&gt; Always. If you can&amp;rsquo;t read the path out loud and feel confident, don&amp;rsquo;t run the command.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid &lt;code&gt;sudo&lt;/code&gt; unless you need it.&lt;/strong&gt; Running &lt;code&gt;rm&lt;/code&gt; as root can wipe system files. Use it sparingly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preview with &lt;code&gt;ls&lt;/code&gt; first.&lt;/strong&gt; Run &lt;code&gt;ls /path/to/directory&lt;/code&gt; before &lt;code&gt;rm -r /path/to/directory&lt;/code&gt;. Confirm you&amp;rsquo;re looking at the right files.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="why-it-matters-4"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;Cleanup is part of running a server. Logs grow, downloads pile up, old configs linger. &lt;code&gt;rm&lt;/code&gt; keeps the system tidy. But every Linux user has an &lt;code&gt;rm -rf&lt;/code&gt; horror story. Don&amp;rsquo;t earn yours.&lt;/p&gt;
&lt;h3 id="example-workflow-1"&gt;Example Workflow
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Delete a single file:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm Movie_I_Hate.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Remove a directory and its contents:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -r ~/Downloads/temp_folder
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;Clean all &lt;code&gt;.tmp&lt;/code&gt; files in a directory:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm *.tmp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="6-cp---copy-files-and-directories"&gt;6. &lt;code&gt;cp&lt;/code&gt; - Copy Files and Directories
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cp&lt;/code&gt; copies files and directories. Backups, duplicates, config snapshots before you mess with something. You&amp;rsquo;ll reach for this one constantly.&lt;/p&gt;
&lt;h3 id="basic-usage-5"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Copy a file:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp &amp;lt;source-file&amp;gt; &amp;lt;destination&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp config.conf backup_config.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This copies &lt;code&gt;config.conf&lt;/code&gt; to &lt;code&gt;backup_config.conf&lt;/code&gt; in your current directory. Cheap insurance before editing.&lt;/p&gt;
&lt;h3 id="copying-multiple-files"&gt;Copying Multiple Files
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp file1.txt file2.txt /path/to/destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="copying-directories"&gt;Copying Directories
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;cp&lt;/code&gt; won&amp;rsquo;t copy directories without the recursive &lt;code&gt;-r&lt;/code&gt; flag:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -r /source/directory /destination/directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That duplicates the whole tree, subdirectories and all.&lt;/p&gt;
&lt;h3 id="practical-use-cases-3"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Snapshot a config before editing:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp config.yaml config_backup.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you break the edit, you have a known-good copy to restore.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copy media between folders or drives:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /home/user/downloads/movie.mkv /media/Movies/Movie_Name/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="useful-options-3"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-i&lt;/code&gt; (Interactive):&lt;/strong&gt; Asks before overwriting existing files.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -i file1.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v&lt;/code&gt; (Verbose):&lt;/strong&gt; Shows each file as it copies. Helpful on large operations.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -v file1.txt file2.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-u&lt;/code&gt; (Update):&lt;/strong&gt; Only copies if the source is newer or the destination file doesn&amp;rsquo;t exist. Great for syncing.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -u file1.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-p&lt;/code&gt; (Preserve):&lt;/strong&gt; Keeps timestamps, permissions, and ownership.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp -p file1.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tips-1"&gt;Tips
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Preview first:&lt;/strong&gt; Use &lt;code&gt;ls&lt;/code&gt; to confirm both source and destination paths before running &lt;code&gt;cp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid overwrites:&lt;/strong&gt; Use &lt;code&gt;-i&lt;/code&gt; to prompt before clobbering, or &lt;code&gt;-n&lt;/code&gt; (no-clobber) to skip overwrites entirely.&lt;/p&gt;
&lt;h3 id="why-it-matters-5"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;Anytime you&amp;rsquo;re about to edit something important, copy it first. &lt;code&gt;cp config.yaml config.yaml.bak&lt;/code&gt; takes one second and has saved me more times than I can count. Cheap insurance.&lt;/p&gt;
&lt;h2 id="7-mv---move-or-rename-files-and-directories"&gt;7. &lt;code&gt;mv&lt;/code&gt; - Move or Rename Files and Directories
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; does two jobs. It moves files and directories, and it renames them. Same command, depending on where the destination points.&lt;/p&gt;
&lt;h3 id="basic-usage-6"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Move a file:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv &amp;lt;source-file&amp;gt; &amp;lt;destination&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv movie.mkv /media/Movies/Movie_Name/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This moves &lt;code&gt;movie.mkv&lt;/code&gt; into the &lt;code&gt;Movie_Name&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rename a file:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv &amp;lt;old-name&amp;gt; &amp;lt;new-name&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv Movie_Name.mkv Movie_Name_2022.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Same command, different destination. Linux treats a rename as a move within the same directory.&lt;/p&gt;
&lt;h3 id="moving-multiple-files"&gt;Moving Multiple Files
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv file1.txt file2.txt file3.txt /destination/directory/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="moving-directories"&gt;Moving Directories
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Move an entire directory:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv /source/directory /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whole tree, one command.&lt;/p&gt;
&lt;h3 id="useful-options-4"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-i&lt;/code&gt; (Interactive):&lt;/strong&gt; Prompts before overwriting.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv -i file.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v&lt;/code&gt; (Verbose):&lt;/strong&gt; Shows each move or rename.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv -v file.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-n&lt;/code&gt; (No-Clobber):&lt;/strong&gt; Won&amp;rsquo;t overwrite existing files at the destination.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv -n file.txt /destination/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tips-2"&gt;Tips
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Preview first:&lt;/strong&gt; Use &lt;code&gt;ls&lt;/code&gt; or &lt;code&gt;find&lt;/code&gt; to check what you&amp;rsquo;re about to move. A bad &lt;code&gt;mv&lt;/code&gt; on a system file can break things in unexpected ways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid overwrites:&lt;/strong&gt; &lt;code&gt;-i&lt;/code&gt; or &lt;code&gt;-n&lt;/code&gt; will save you when the destination has a file you forgot about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Be careful with &lt;code&gt;sudo&lt;/code&gt;:&lt;/strong&gt; Moving system files as root can break boot. Read your command twice before you hit Enter.&lt;/p&gt;
&lt;h3 id="why-it-matters-6"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;mv&lt;/code&gt; is fast and irreversible. Move a config to the wrong place and your daemon won&amp;rsquo;t start. But used carefully, it&amp;rsquo;s how you keep a server organized. Rename a misnamed download, shuffle a folder to a new drive, restructure a media library. All &lt;code&gt;mv&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="8-chmod---change-file-permissions"&gt;8. &lt;code&gt;chmod&lt;/code&gt; - Change File Permissions
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;chmod&lt;/code&gt; controls who can read, write, and execute a file. It&amp;rsquo;s how Linux keeps users out of each other&amp;rsquo;s stuff and stops accidental damage to system files. If you&amp;rsquo;ve ever copied a script and gotten &amp;ldquo;Permission denied,&amp;rdquo; &lt;code&gt;chmod&lt;/code&gt; is the fix.&lt;/p&gt;
&lt;h3 id="understanding-file-permissions"&gt;Understanding File Permissions
&lt;/h3&gt;&lt;p&gt;Before touching &lt;code&gt;chmod&lt;/code&gt;, look at what permissions look like. Run &lt;code&gt;ls -l&lt;/code&gt; on a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-rwxr-xr-- &lt;span class="m"&gt;1&lt;/span&gt; user group &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;23&lt;/span&gt; 10:00 script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three letters, three categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;r&lt;/code&gt; (read):&lt;/strong&gt; View the file&amp;rsquo;s contents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;w&lt;/code&gt; (write):&lt;/strong&gt; Modify the file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;x&lt;/code&gt; (execute):&lt;/strong&gt; Run the file as a program or script.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three categories of users:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Owner&lt;/strong&gt; (&lt;code&gt;rwx&lt;/code&gt;) - Full access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; (&lt;code&gt;r-x&lt;/code&gt;) - Read and execute for group members.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Others&lt;/strong&gt; (&lt;code&gt;r--&lt;/code&gt;) - Everyone else can only read.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="basic-usage-7"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Symbolic notation:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &amp;lt;permissions&amp;gt; &amp;lt;file&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Give the owner execute permission:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod u+x script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Symbolic targets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;u&lt;/code&gt; (user):&lt;/strong&gt; The file&amp;rsquo;s owner.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;g&lt;/code&gt; (group):&lt;/strong&gt; Users in the file&amp;rsquo;s group.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;o&lt;/code&gt; (others):&lt;/strong&gt; Everyone else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;a&lt;/code&gt; (all):&lt;/strong&gt; Applies to &lt;code&gt;u&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, and &lt;code&gt;o&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Modifiers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;+&lt;/code&gt; (add):&lt;/strong&gt; Adds a permission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-&lt;/code&gt; (remove):&lt;/strong&gt; Removes a permission.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;=&lt;/code&gt; (set):&lt;/strong&gt; Sets a permission exactly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add read and write for the group:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod g+rw file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Remove execute for others:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod o-x script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Octal notation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Faster once you know it. Numbers map to permission bits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;4&lt;/code&gt; (read):&lt;/strong&gt; &lt;code&gt;r--&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;2&lt;/code&gt; (write):&lt;/strong&gt; &lt;code&gt;-w-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;1&lt;/code&gt; (execute):&lt;/strong&gt; &lt;code&gt;--x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add them up. &lt;code&gt;7 = rwx&lt;/code&gt;, &lt;code&gt;6 = rw-&lt;/code&gt;, &lt;code&gt;5 = r-x&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;755&lt;/span&gt; script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That sets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Owner:&lt;/strong&gt; Read, write, execute (&lt;code&gt;7 = rwx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group:&lt;/strong&gt; Read, execute (&lt;code&gt;5 = r-x&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Others:&lt;/strong&gt; Read, execute (&lt;code&gt;5 = r-x&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-use-cases-4"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Make a script executable:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x my_script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Lock down a sensitive file:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; private_file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Only the owner can read or write. Everyone else is shut out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Open a file for group collaboration:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod g+rw shared_file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="useful-options-5"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-R&lt;/code&gt; (Recursive):&lt;/strong&gt; Applies permissions to a directory and everything inside.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod -R &lt;span class="m"&gt;755&lt;/span&gt; /path/to/directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--reference&lt;/code&gt;:&lt;/strong&gt; Copies permissions from one file to another.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod --reference&lt;span class="o"&gt;=&lt;/span&gt;source_file target_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="example-workflow-2"&gt;Example Workflow
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check current permissions:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Modify permissions.&lt;/strong&gt; Grant read and write to the owner and group, deny everything to others:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;660&lt;/span&gt; file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;Open a directory and everything inside:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod -R &lt;span class="m"&gt;755&lt;/span&gt; /media/Movies
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="tips-3"&gt;Tips
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Test on one file first.&lt;/strong&gt; Don&amp;rsquo;t run &lt;code&gt;chmod -R&lt;/code&gt; on a directory tree until you&amp;rsquo;ve confirmed the result on a single file. Recursive permission changes are hard to unwind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don&amp;rsquo;t use 777.&lt;/strong&gt; It opens the file to every user on the system. I know it makes the &amp;ldquo;Permission denied&amp;rdquo; error go away. It also makes every security audit cry. For shared media folders I use &lt;code&gt;770&lt;/code&gt;. Owner and group get full access, everyone else gets nothing. Files stay protected and invisible to outsiders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check regularly:&lt;/strong&gt; Use &lt;code&gt;ls -l&lt;/code&gt; to confirm permissions are what you expect, especially after big copies or moves.&lt;/p&gt;
&lt;h3 id="why-it-matters-7"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;Permissions are where Linux security starts. &lt;code&gt;chmod&lt;/code&gt; is how you control it. Get this one wrong on a media server and either nothing works or the world can read your config files. Take the time to understand it.&lt;/p&gt;
&lt;h2 id="9-chown---change-file-ownership"&gt;9. &lt;code&gt;chown&lt;/code&gt; - Change File Ownership
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;chown&lt;/code&gt; changes who owns a file or directory. Every file in Linux has two ownership attributes: a &lt;strong&gt;user&lt;/strong&gt; and a &lt;strong&gt;group&lt;/strong&gt;. &lt;code&gt;chown&lt;/code&gt; lets you reassign both. Critical for multi-user systems and for fixing the &amp;ldquo;why can&amp;rsquo;t Plex read this folder&amp;rdquo; problem after a copy from somewhere else.&lt;/p&gt;
&lt;h3 id="basic-usage-8"&gt;Basic Usage
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Change the owner of a file:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown &amp;lt;new-owner&amp;gt; &amp;lt;file&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown kryptikwurm movie.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That sets the owner of &lt;code&gt;movie.mkv&lt;/code&gt; to &lt;code&gt;kryptikwurm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Change both owner and group:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown &amp;lt;new-owner&amp;gt;:&amp;lt;new-group&amp;gt; &amp;lt;file&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown kryptikwurm:media movie.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Owner becomes &lt;code&gt;kryptikwurm&lt;/code&gt;, group becomes &lt;code&gt;media&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="changing-ownership-of-directories"&gt;Changing Ownership of Directories
&lt;/h3&gt;&lt;p&gt;Use &lt;code&gt;-R&lt;/code&gt; (recursive) to apply ownership to a directory and everything inside:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R kryptikwurm:media /media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That changes ownership on &lt;code&gt;/media&lt;/code&gt; and every file and subfolder inside it.&lt;/p&gt;
&lt;h3 id="useful-options-6"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-R&lt;/code&gt; (Recursive):&lt;/strong&gt; Applies changes to a directory tree.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R kryptikwurm:media /media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-v&lt;/code&gt; (Verbose):&lt;/strong&gt; Prints each change as it happens.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -v kryptikwurm:media movie.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="checking-file-ownership"&gt;Checking File Ownership
&lt;/h3&gt;&lt;p&gt;Use &lt;code&gt;ls -l&lt;/code&gt; before and after:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;-rw-r--r-- &lt;span class="m"&gt;1&lt;/span&gt; user group &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;23&lt;/span&gt; 12:00 file.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first name is the owner, the second is the group.&lt;/p&gt;
&lt;h3 id="safety-tips-1"&gt;Safety Tips
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Be careful with &lt;code&gt;sudo&lt;/code&gt;:&lt;/strong&gt; Double-check the path. A &lt;code&gt;chown&lt;/code&gt; on the wrong directory can lock you out of system files until you fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No wildcards on root paths:&lt;/strong&gt; &lt;code&gt;chown -R user:group /*&lt;/code&gt; will trash your system. Don&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preview first:&lt;/strong&gt; Use &lt;code&gt;ls -l&lt;/code&gt; to inspect ownership before changing it.&lt;/p&gt;
&lt;h3 id="why-it-matters-8"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;Proper ownership is how Linux decides what a service can read and write. Use &lt;code&gt;chown&lt;/code&gt; to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let the right user account access your media folders.&lt;/li&gt;
&lt;li&gt;Hand a shared folder to a group so multiple users can collaborate.&lt;/li&gt;
&lt;li&gt;Fix permission errors after migrating data between machines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get ownership right and most of your &amp;ldquo;Plex can&amp;rsquo;t see this folder&amp;rdquo; or &amp;ldquo;Sonarr can&amp;rsquo;t write to this directory&amp;rdquo; headaches disappear.&lt;/p&gt;
&lt;h2 id="10-df---display-disk-space-usage"&gt;10. &lt;code&gt;df&lt;/code&gt; - Display Disk Space Usage
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;df&lt;/code&gt; shows how much disk space you have. Short for &amp;ldquo;disk free.&amp;rdquo; Run it before you launch a big download. Run it when a service mysteriously stops writing. Run it when the server feels slow. It&amp;rsquo;s a thirty-character command that catches a lot of problems early.&lt;/p&gt;
&lt;h3 id="basic-usage-9"&gt;Basic Usage
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem 1K-blocks Used Available Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 &lt;span class="m"&gt;500000000&lt;/span&gt; &lt;span class="m"&gt;250000000&lt;/span&gt; &lt;span class="m"&gt;250000000&lt;/span&gt; 50% /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs &lt;span class="m"&gt;4000000&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4000000&lt;/span&gt; 0% /dev/shm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="key-fields"&gt;Key Fields
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Filesystem:&lt;/strong&gt; The disk or storage device (&lt;code&gt;/dev/sda1&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1K-blocks:&lt;/strong&gt; Total space in 1KB blocks. Ugly. Use &lt;code&gt;-h&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Used:&lt;/strong&gt; Space currently in use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Available:&lt;/strong&gt; Space free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use%:&lt;/strong&gt; Percentage in use. The number that matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mounted on:&lt;/strong&gt; Where the filesystem is mounted in the directory tree.&lt;/p&gt;
&lt;h3 id="useful-options-7"&gt;Useful Options
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-h&lt;/code&gt; (Human-Readable):&lt;/strong&gt; Show sizes in MB, GB, TB.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem Size Used Avail Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 500G 250G 250G 50% /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;-T&lt;/code&gt; (Show Filesystem Type):&lt;/strong&gt; Include the filesystem type (ext4, xfs, btrfs).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -T
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem Type 1K-blocks Used Available Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 ext4 &lt;span class="m"&gt;500000000&lt;/span&gt; &lt;span class="m"&gt;250000000&lt;/span&gt; &lt;span class="m"&gt;250000000&lt;/span&gt; 50% /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="practical-use-cases-5"&gt;Practical Use Cases
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Check storage on a media server:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Spot filesystems nearing capacity.&lt;/strong&gt; A 98% full disk will silently break Plex transcoding and Sonarr imports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem Size Used Avail Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 500G 490G 10G 98% /
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Verify external drives or network shares are mounted:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h /media/usb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the line doesn&amp;rsquo;t appear, the mount failed.&lt;/p&gt;
&lt;h3 id="tips-4"&gt;Tips
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Always use &lt;code&gt;-h&lt;/code&gt;.&lt;/strong&gt; The default output is unreadable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check before big writes.&lt;/strong&gt; Before you rip a Blu-ray to disk or pull down a 50GB Linux ISO, run &lt;code&gt;df -h&lt;/code&gt; and confirm you have room.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confirm mounts after reboot.&lt;/strong&gt; Use &lt;code&gt;df&lt;/code&gt; to verify external drives and NFS shares came back up correctly. Half the time a &amp;ldquo;missing files&amp;rdquo; panic is actually a dropped mount.&lt;/p&gt;
&lt;h3 id="why-it-matters-9"&gt;Why It Matters
&lt;/h3&gt;&lt;p&gt;A full disk is one of the most common reasons services break on a media server. Plex can&amp;rsquo;t write metadata. Sonarr can&amp;rsquo;t import. Backups silently fail. &lt;code&gt;df&lt;/code&gt; is a five-second check that saves hours of debugging.&lt;/p&gt;
&lt;h2 id="in-conclusion"&gt;In Conclusion
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the toolkit. Ten commands. Learn these and you can navigate a Linux box, manage files, set permissions, control ownership, and keep an eye on disk space. Which covers most of what running a media server requires.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re not going to memorize the flags from a blog post. Open a terminal. Make some folders. Move them. Delete the wrong one. Recover. That&amp;rsquo;s how this sticks. The best way to learn Linux is to break a test box and fix it.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re ready for the next step, start chaining these commands together with pipes and writing small shell scripts. That&amp;rsquo;s where Linux stops being a list of commands and starts feeling like a tool you actually own.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t stop at ten. But start here.&lt;/p&gt;</description></item><item><title>Turn an Old Computer Into a Media Server Part 4 How to Configure Jellyfin</title><link>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/</link><pubDate>Mon, 20 Jan 2025 08:05:12 -0700</pubDate><guid>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/</guid><description>&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/oldpc42_hu_a20c4d1af9ec297e.webp" alt="Featured image of post Turn an Old Computer Into a Media Server Part 4 How to Configure Jellyfin" /&gt;&lt;h2 id="introduction"&gt;Introduction
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve done the heavy lifting. Ubuntu Server 24.04 LTS is installed, your storage is partitioned and mounted, SMB shares are working, and Jellyfin is sitting there waiting. Now it&amp;rsquo;s time to point it at your media and finish the wizard so you can watch something.&lt;/p&gt;
&lt;p&gt;This post walks the rest of the way. You&amp;rsquo;ll complete the setup wizard, add your libraries, set a few sane defaults, and confirm playback works. By the end, your old PC will be a working media server that streams to anything on your LAN.&lt;/p&gt;
&lt;h2 id="accessing-the-jellyfin-web-interface"&gt;Accessing the Jellyfin Web Interface
&lt;/h2&gt;&lt;h3 id="step-1-connect-to-jellyfin"&gt;Step 1: Connect to Jellyfin
&lt;/h3&gt;&lt;p&gt;Open a web browser on a device connected to your network.&lt;/p&gt;
&lt;p&gt;Enter the server&amp;rsquo;s IP address followed by the port number &lt;code&gt;:8096&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example:
&lt;code&gt;http://192.168.1.100:8096&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This loads the Jellyfin setup wizard. It should look like this:&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfinwelcome_hu_c10dff0ddd13f598.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfinwelcome_hu_6289942cb9627df6.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfinwelcome_hu_97a74dccce94a227.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfinwelcome_hu_97a74dccce94a227.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfinwelcome_hu_fcc3570b17964041.webp 1600w" width="800" height="182" alt="Jellyfin Welcome Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;If the page doesn&amp;rsquo;t load, check this post: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/troubleshooting-jellyfin-server-access-issues/" &gt;Troubleshooting - Jellyfin Server Access Issues&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="running-the-jellyfin-setup-wizard"&gt;Running the Jellyfin Setup Wizard
&lt;/h2&gt;&lt;h3 id="step-1-choose-your-language-and-region"&gt;Step 1: Choose Your Language and Region
&lt;/h3&gt;&lt;p&gt;Pick your preferred language.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_language_hu_401c57f70f8918d1.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_language_hu_b70e7a5fba620205.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_language_hu_3c5377fbd92452e3.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_language_hu_3c5377fbd92452e3.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_language_hu_c6b82506b49ebcd1.webp 1600w" width="800" height="306" alt="Jellyfin Language Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h3 id="step-2-create-an-admin-account"&gt;Step 2: Create an Admin Account
&lt;/h3&gt;&lt;p&gt;Pick a strong admin username and password. This is the account that controls everything on the server, so don&amp;rsquo;t reuse a password you&amp;rsquo;ve used anywhere else.&lt;/p&gt;
&lt;h4 id="tips-for-creating-a-strong-username-and-password"&gt;Tips for Creating a Strong Username and Password
&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use Unique Credentials&lt;/strong&gt;: Skip common usernames like &amp;ldquo;jellyfin&amp;rdquo; and passwords like &amp;ldquo;123456.&amp;rdquo; Pick a unique username, and for the password use a mix of letters, numbers, and special characters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make It Long&lt;/strong&gt;: Passwords should be at least 12 characters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Avoid Predictable Words&lt;/strong&gt;: Don&amp;rsquo;t use easily guessed information like your name, birthday, or server name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use a Password Manager&lt;/strong&gt;: If remembering complex passwords is a pain, a password manager can generate and store them securely for you.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_username_hu_f04ddbffe3bc09de.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_username_hu_d85bcd69931cdd90.avif 1288w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_username_hu_df0f273d8126e219.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_username_hu_df0f273d8126e219.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_username_hu_a5c86df2063ad742.webp 1288w" width="800" height="457" alt="Jellyfin Username Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h3 id="step-3-add-media-libraries"&gt;Step 3: Add Media Libraries
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;Click &amp;ldquo;Add Media Library.&amp;rdquo;&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_setup_hu_3148cfb983f165c2.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_setup_hu_971b275de0aaca9f.avif 1322w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_setup_hu_a69dc3fe4a3f93c0.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_setup_hu_a69dc3fe4a3f93c0.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_setup_hu_8eceeb9c0439ef9d.webp 1322w" width="800" height="382" alt="Jellyfin Library Setup Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Pick the content type (Movies, TV Shows, Music, etc.).&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_type_hu_249e9d543a7c716d.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_type_hu_ddcf4abe817ea2fa.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_type_hu_845d78dde7caea3c.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_type_hu_845d78dde7caea3c.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_type_hu_e9a067cb0b281559.webp 1600w" width="800" height="162" alt="Jellyfin Library Type Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Click the &lt;code&gt;+&lt;/code&gt; next to &amp;ldquo;Folders&amp;rdquo; and browse to the folder where the media is stored (e.g., &lt;code&gt;/mnt/media/Movies&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_path_hu_b348510abcc86a91.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_path_hu_4590eadadadedb2a.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_path_hu_4418922cc75779bd.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_path_hu_4418922cc75779bd.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_path_hu_5090996b32bab19d.webp 1600w" width="800" height="195" alt="Jellyfin Library Path Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="4"&gt;
&lt;li&gt;The defaults are fine for an initial setup. Leave them alone.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_movies_hu_aa01fd1d99a34e34.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_movies_hu_b52af43086b2f5a3.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_movies_hu_6a04721c3ce839bc.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_movies_hu_6a04721c3ce839bc.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_libraries_movies_hu_8be3419d7cc25f6.webp 1600w" width="800" height="541" alt="Jellyfin Library Options Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="5"&gt;
&lt;li&gt;Repeat for &lt;code&gt;/mnt/media/Shows&lt;/code&gt; and &lt;code&gt;/mnt/media/Music&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="step-4-configure-metadata-options"&gt;Step 4: Configure Metadata Options
&lt;/h3&gt;&lt;p&gt;Metadata pulls in the cover art, plot summaries, cast lists, and ratings for your movies, shows, and music. It&amp;rsquo;s what turns a folder of filenames into something that looks like a streaming service.&lt;/p&gt;
&lt;p&gt;For now, accept the defaults. You can fine-tune later.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_metadata_hu_459a1e29c74c38bb.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_metadata_hu_7fe8f5be551a37f4.avif 1185w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_metadata_hu_d53e951fb54de1d7.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_metadata_hu_d53e951fb54de1d7.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_metadata_hu_4c8da307e0da1bc.webp 1185w" width="800" height="315" alt="Jellyfin Metadata Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h3 id="step-5-remote-access-optional"&gt;Step 5: Remote Access (Optional)
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Not recommended.&lt;/strong&gt; If you want remote access, you&amp;rsquo;ll need to forward Jellyfin&amp;rsquo;s port (default &lt;code&gt;8096&lt;/code&gt;) on your router to the server&amp;rsquo;s internal IP. That exposes the server to the public internet. If you do this, use a VPN or put it behind a reverse proxy with HTTPS and a real certificate. Don&amp;rsquo;t open port 8096 to the world and hope for the best.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_remote_hu_a8fd6a839937dc3d.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_remote_hu_5f4e45547f8e237c.avif 1356w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_remote_hu_4cb96d94c81cc2aa.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_remote_hu_4cb96d94c81cc2aa.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_remote_hu_d4d2300a98c20944.webp 1356w" width="800" height="254" alt="Jellyfin Remote Access Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h3 id="step-6-finish-the-wizard"&gt;Step 6: Finish the Wizard
&lt;/h3&gt;&lt;p&gt;Click &amp;ldquo;Finish.&amp;rdquo; The server will start scanning your media folders and populating the libraries.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_done_hu_8b96ae907f171407.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_done_hu_80793d2d4dea1bc7.avif 1238w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_done_hu_78c1c35450ca1111.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_done_hu_78c1c35450ca1111.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_done_hu_87aa0b6fdcb7d8df.webp 1238w" width="800" height="192" alt="Jellyfin Finished Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;If everything went well, you should see the web interface with your media listed.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_home_hu_4efb1844cc847380.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_home_hu_fc6a707773168045.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_home_hu_14015b300aa8bb24.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_home_hu_14015b300aa8bb24.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/jellyfin_home_hu_466dafee1bd454fc.webp 1600w" width="800" height="414" alt="Jellyfin Home Screen" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h2 id="testing-your-jellyfin-setup"&gt;Testing Your Jellyfin Setup
&lt;/h2&gt;&lt;h3 id="step-1-open-the-jellyfin-client"&gt;Step 1: Open the Jellyfin Client
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You can use the web browser, or install the Jellyfin app on the device you want to watch on (phone, tablet, TV, streaming stick).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Note: Right now this is only accessible while you&amp;rsquo;re connected to your &lt;strong&gt;local&lt;/strong&gt; network.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Jellyfin apps: download from your platform&amp;rsquo;s app store, or grab them from the official site.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-2-test-playback"&gt;Step 2: Test Playback
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Play a sample movie or TV show to confirm the server is working. You can do it straight from the web browser, but some browsers choke on certain codecs. For a cleaner test, use the official Jellyfin client app. It handles more formats natively and doesn&amp;rsquo;t lean on the browser for decoding.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Client apps: &lt;a class="link" href="https://jellyfin.org/downloads/" target="_blank" rel="noopener"
&gt;Jellyfin Client Downloads&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s Jellyfin up and running with user accounts, metadata, and media libraries. Your server is now streaming to anything on your network.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s plenty more to dig into when you&amp;rsquo;re ready. Themes, plugins, hardware transcoding, user permissions, and remote access are all next steps worth exploring. Get comfortable with what you&amp;rsquo;ve built first, then pick one thing at a time.&lt;/p&gt;
&lt;p&gt;Time to grab some popcorn and watch something on the server you built.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 1 - Installing Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 2 - Storage and SMB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 3 - Installing Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Troubleshooting - Jellyfin Server Access Issues</title><link>https://diymediaserver.com/post/troubleshooting-jellyfin-server-access-issues/</link><pubDate>Mon, 20 Jan 2025 07:44:15 -0700</pubDate><guid>https://diymediaserver.com/post/troubleshooting-jellyfin-server-access-issues/</guid><description>&lt;img src="https://diymediaserver.com/post/troubleshooting-jellyfin-server-access-issues/jellyfin_troubleshoot2_hu_f276535b5b9abd6b.webp" alt="Featured image of post Troubleshooting - Jellyfin Server Access Issues" /&gt;&lt;p&gt;You set up Jellyfin, you&amp;rsquo;re ready to watch something, and the browser hangs on &lt;code&gt;http://your-server-ip:8096&lt;/code&gt;. Frustrating, but fixable. Most of the time it&amp;rsquo;s one of a handful of dumb things, and you can work through them in a few minutes. Here&amp;rsquo;s the order I check them in.&lt;/p&gt;
&lt;h3 id="1-double-check-the-ip-address"&gt;1. Double-Check the IP Address
&lt;/h3&gt;&lt;p&gt;First, make sure you&amp;rsquo;re hitting the right address. Typos here waste more time than anything else.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the server, open a terminal and run:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Find the &lt;code&gt;inet&lt;/code&gt; line under your active interface (like &lt;code&gt;eth0&lt;/code&gt; or &lt;code&gt;wlan0&lt;/code&gt;). It&amp;rsquo;ll look like &lt;code&gt;192.168.1.100&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the address you should be typing into the browser, followed by &lt;code&gt;:8096&lt;/code&gt;. If your server has more than one interface, make sure you&amp;rsquo;re using the one that&amp;rsquo;s on the same network as your client.&lt;/p&gt;
&lt;h3 id="2-make-sure-jellyfin-is-running"&gt;2. Make Sure Jellyfin Is Running
&lt;/h3&gt;&lt;p&gt;If the service is dead, the browser has nothing to talk to. Check it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for &lt;code&gt;Active: active (running)&lt;/code&gt;. If it isn&amp;rsquo;t, start it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And while you&amp;rsquo;re there, make sure it&amp;rsquo;ll come back up after a reboot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve lost more time than I want to admit to a Jellyfin install that wasn&amp;rsquo;t enabled, then quietly stayed dead after a power blip.&lt;/p&gt;
&lt;h3 id="3-test-connectivity"&gt;3. Test Connectivity
&lt;/h3&gt;&lt;p&gt;Now check that the client can actually reach the server. From the client device:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ping &amp;lt;server-ip&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Swap &lt;code&gt;&amp;lt;server-ip&amp;gt;&lt;/code&gt; for the address from step 1 (e.g. &lt;code&gt;192.168.1.100&lt;/code&gt;). Replies coming back? Good, the network path is fine. No replies? You&amp;rsquo;ve got a firewall, VLAN, or wifi-isolation problem between the two machines, and that&amp;rsquo;s where to focus next.&lt;/p&gt;
&lt;h3 id="4-reboot-the-server"&gt;4. Reboot the Server
&lt;/h3&gt;&lt;p&gt;Yes, it&amp;rsquo;s a cliche. Yes, it works. A reboot clears stuck network state and restarts every service cleanly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wait for the box to come back, then try the browser again.&lt;/p&gt;
&lt;h3 id="5-check-browser-compatibility"&gt;5. Check Browser Compatibility
&lt;/h3&gt;&lt;p&gt;The Jellyfin web UI expects a modern browser. Chrome, Firefox, and Edge all work fine. If you&amp;rsquo;re on something old or weird, update it or try a different one before you go any deeper into the logs.&lt;/p&gt;
&lt;h3 id="when-all-else-fails-check-the-logs"&gt;When All Else Fails: Check the Logs
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;ve worked through the list and you&amp;rsquo;re still locked out, the logs will usually tell you why. Pull them with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo journalctl -u jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Scroll through the recent output and look for errors or warnings. Port conflicts, permission problems, and missing config files all show up here. That&amp;rsquo;s normally where the real answer is hiding.&lt;/p&gt;
&lt;p&gt;Work the list top to bottom and you&amp;rsquo;ll catch the problem somewhere along the way. Most Jellyfin access issues are network, service, or browser, not Jellyfin itself.&lt;/p&gt;</description></item><item><title>Turn an Old Computer Into a Media Server Part 3 Installing Jellyfin and Its Dependencies</title><link>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/</link><pubDate>Sat, 18 Jan 2025 07:00:43 -0700</pubDate><guid>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/</guid><description>&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/oldpc32_hu_8159aac330b6fbfb.webp" alt="Featured image of post Turn an Old Computer Into a Media Server Part 3 Installing Jellyfin and Its Dependencies" /&gt;&lt;h3 id="recap-your-journey-so-far"&gt;Recap: Your Journey So Far
&lt;/h3&gt;&lt;p&gt;In the previous posts, we turned an old PC into a server running &lt;strong&gt;Ubuntu Server 24.04 LTS&lt;/strong&gt;. Along the way, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Covered why an old PC is a smart starting point. It&amp;rsquo;s cheap, eco-friendly, and forgiving for beginners.&lt;/li&gt;
&lt;li&gt;Walked through the minimum hardware Jellyfin needs. You don&amp;rsquo;t need a new rig to run a media server.&lt;/li&gt;
&lt;li&gt;Installed Ubuntu Server step by step, from flashing a USB drive to finishing the installer.&lt;/li&gt;
&lt;li&gt;Added storage and got the drives partitioned, formatted, and mounted.&lt;/li&gt;
&lt;li&gt;Set up a Samba share so you can drop files onto the server from any machine on your network.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The server is ready. Time to put &lt;strong&gt;Jellyfin&lt;/strong&gt; on it. I ran every step in this post on a fresh Ubuntu Server 24.04 LTS install, and the same commands work on 22.04 and 23.10 with no changes.&lt;/p&gt;
&lt;h3 id="what-is-jellyfin"&gt;What Is Jellyfin?
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Jellyfin&lt;/strong&gt; is a free, open-source media server that organizes, manages, and streams your collection to any device. Think of it as your own Netflix, but you own the data and the server. Movies, TV, music, photos, all of it lives in one place you can hit from your phone, smart TV, computer, or a web browser.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what we&amp;rsquo;ll cover:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Preparing Ubuntu Server.&lt;/strong&gt; Patch the system and install &lt;code&gt;curl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adding the Jellyfin repository.&lt;/strong&gt; Pull the official GPG key and source list so &lt;code&gt;apt&lt;/code&gt; knows where to look.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Installing Jellyfin.&lt;/strong&gt; Run &lt;code&gt;apt install&lt;/code&gt;, then enable, start, and verify the service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessing Jellyfin for the first time.&lt;/strong&gt; Find the server IP, hit port 8096, and load the setup wizard.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="section-1-preparing-your-server-for-jellyfin"&gt;Section 1: Preparing Your Server for Jellyfin
&lt;/h2&gt;&lt;h3 id="ssh-into-your-new-server"&gt;SSH Into Your New Server
&lt;/h3&gt;&lt;p&gt;Not sure how to use SSH? Read this first: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/" &gt;Master the Basics - How to SSH Into a Linux Server&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="steps-to-connect-using-ssh"&gt;Steps to Connect Using SSH
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Open PowerShell on Windows&lt;/strong&gt; and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Powershell" data-lang="Powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ssh&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;@&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;server-ip&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example from Part 1:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-PowerShell" data-lang="PowerShell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ssh&lt;/span&gt; &lt;span class="n"&gt;kryptikwurm&lt;/span&gt;&lt;span class="nv"&gt;@192&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Authenticate.&lt;/strong&gt; Type the server password when prompted. You won&amp;rsquo;t see any characters as you type. That&amp;rsquo;s normal. SSH hides the input on purpose.&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;re in, you&amp;rsquo;ve got a full remote shell. Time to install Jellyfin.&lt;/p&gt;
&lt;h4 id="install-system-updates-and-curl"&gt;Install System Updates and Curl
&lt;/h4&gt;&lt;p&gt;&lt;strong&gt;Update the system first.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Patches close security holes and pull in the bug fixes that keep new software from breaking on day one. Run this before anything else:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Install curl.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; is a command-line tool for moving data between your machine and a remote server. It speaks HTTP, HTTPS, FTP, and a pile of other protocols, and you&amp;rsquo;ll use it to grab the Jellyfin GPG key in the next step. It might already be on your system. Install it anyway. It&amp;rsquo;s harmless if it&amp;rsquo;s already there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install curl -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="section-2-adding-the-jellyfin-repository"&gt;Section 2: Adding the Jellyfin Repository
&lt;/h2&gt;&lt;p&gt;Adding the Jellyfin repository pulls Jellyfin straight from its official source. You get the latest stable release, security fixes show up through normal &lt;code&gt;apt upgrade&lt;/code&gt; runs, and you never have to chase a &lt;code&gt;.deb&lt;/code&gt; file off a forum post.&lt;/p&gt;
&lt;p&gt;Run these one at a time.&lt;/p&gt;
&lt;p&gt;First, fetch and store the GPG key. &lt;code&gt;apt&lt;/code&gt; uses it to verify Jellyfin packages haven&amp;rsquo;t been tampered with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsSL https://repo.jellyfin.org/ubuntu/jellyfin_team.gpg.key &lt;span class="p"&gt;|&lt;/span&gt; sudo gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, register the repository. This drops a file into &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt; so Ubuntu knows where to look for Jellyfin and its updates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deb [signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/ubuntu noble main&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; sudo tee /etc/apt/sources.list.d/jellyfin.list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now refresh the package index. &lt;code&gt;sudo apt update&lt;/code&gt; doesn&amp;rsquo;t install or upgrade anything. It tells your system what&amp;rsquo;s available so the install step finds Jellyfin:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If it worked, you&amp;rsquo;ll see a line like this in the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;repo.jellyfin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ubuntu&lt;/span&gt; &lt;span class="n"&gt;noble&lt;/span&gt; &lt;span class="n"&gt;InRelease&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="section-3-installing-jellyfin"&gt;Section 3: Installing Jellyfin
&lt;/h2&gt;&lt;p&gt;Use &lt;code&gt;apt&lt;/code&gt; to install Jellyfin:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install jellyfin -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Enable the service so it starts at boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Start it now, without rebooting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify it&amp;rsquo;s running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl status jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;re looking for a line like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you see &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;inactive (dead)&lt;/code&gt; instead, check &lt;code&gt;journalctl -u jellyfin -n 50&lt;/code&gt; for the error before moving on.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinservice_hu_8728e06bc42d214b.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinservice_hu_1483ff7914543d11.avif 1021w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinservice_hu_b122e49e6541bb49.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinservice_hu_b122e49e6541bb49.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinservice_hu_a660ff914c0ee4e0.webp 1021w" width="800" height="201" alt="Jellyfin Service Image" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h2 id="section-4-creating-groups-and-permissions"&gt;Section 4: Creating Groups and Permissions
&lt;/h2&gt;&lt;p&gt;Jellyfin runs as its own &lt;code&gt;jellyfin&lt;/code&gt; user, and that user needs read access to your media folder. The cleanest way to handle this is a shared group both &lt;code&gt;jellyfin&lt;/code&gt; and your login user belong to.&lt;/p&gt;
&lt;p&gt;Create the group:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo groupadd media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add the &lt;code&gt;jellyfin&lt;/code&gt; user and your own user to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -aG media jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo usermod -aG media &lt;span class="k"&gt;$(&lt;/span&gt;whoami&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Change the group owner on the media tree so anything in &lt;code&gt;/mnt/media&lt;/code&gt; is readable by the group:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R :media /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You may also need &lt;code&gt;sudo chmod -R g+rX /mnt/media&lt;/code&gt; if existing files don&amp;rsquo;t have group-read set. And you&amp;rsquo;ll need to log out and back in (or run &lt;code&gt;newgrp media&lt;/code&gt;) before your shell sees the new group membership.&lt;/p&gt;
&lt;h2 id="section-5-accessing-jellyfin-for-the-first-time"&gt;Section 5: Accessing Jellyfin for the First Time
&lt;/h2&gt;&lt;p&gt;The server&amp;rsquo;s up. You&amp;rsquo;ve got the IP. Open a browser and hit it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open a web browser.&lt;/strong&gt; Use any modern browser. Chrome, Firefox, Edge, Safari. It doesn&amp;rsquo;t matter. Use a device on the same network as the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enter the address.&lt;/strong&gt; In the address bar, type the following, swapping in your server&amp;rsquo;s IP:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;192.168.1.200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8096&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="3"&gt;
&lt;li&gt;&lt;strong&gt;Verify the Jellyfin interface loads.&lt;/strong&gt; You should land on the Jellyfin setup wizard. That confirms the service is running and your network can reach it on port 8096.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the page doesn&amp;rsquo;t load, double-check the IP with &lt;code&gt;ip a&lt;/code&gt; on the server, and confirm Jellyfin is listening with &lt;code&gt;sudo ss -tlnp | grep 8096&lt;/code&gt;. Most first-time failures are a typo&amp;rsquo;d IP or a firewall blocking the port.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinwelcome_hu_c10dff0ddd13f598.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinwelcome_hu_6289942cb9627df6.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinwelcome_hu_97a74dccce94a227.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinwelcome_hu_97a74dccce94a227.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/jellyfinwelcome_hu_fcc3570b17964041.webp 1600w" width="800" height="182" alt="Jellyfin Welcome Image" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h2 id="whats-next"&gt;What’s Next?
&lt;/h2&gt;&lt;p&gt;Jellyfin is installed, running, and reachable. The next post walks through the setup wizard, points Jellyfin at your media folders, and gets the libraries scanning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to configure Jellyfin and start streaming? Head to Part 4 of this series to finish the build.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 1 - Installing Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 2 - Storage and SMB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 4 - Installing Configure Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Turn an Old Computer Into a Media Server Part 2 Adding a Storage Drive and Setting Up SMB Shares</title><link>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/</link><pubDate>Fri, 17 Jan 2025 07:41:16 -0700</pubDate><guid>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/</guid><description>&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/oldpc22_hu_d823617e59c0a5f9.webp" alt="Featured image of post Turn an Old Computer Into a Media Server Part 2 Adding a Storage Drive and Setting Up SMB Shares" /&gt;&lt;h3 id="recap-your-journey-so-far"&gt;Recap: Your Journey So Far
&lt;/h3&gt;&lt;p&gt;In Part 1, we took your old, dusty computer and gave it a new purpose by turning it into a server running &lt;strong&gt;Ubuntu Server 24.04 LTS&lt;/strong&gt;. We covered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Why starting with an old PC is a smart move.&lt;/strong&gt; It&amp;rsquo;s cost-effective, beginner-friendly, and environmentally conscious.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The minimum hardware specs needed to run a Jellyfin server.&lt;/strong&gt; You don&amp;rsquo;t need the latest and greatest hardware to get started.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step-by-step instructions for installing Ubuntu Server.&lt;/strong&gt; From creating a bootable USB drive to finishing the install.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ubuntu Server is installed and ready to go. Your old PC is now a dedicated server, ready to host your Jellyfin media library. But before we touch Jellyfin, you need storage and a way to push files onto it. That means adding drives and setting up an SMB share. Once that&amp;rsquo;s done, you can drop media onto the server straight from a Windows or Linux box, which is the whole point when you&amp;rsquo;re ripping Blu-rays and DVDs on a separate machine.&lt;/p&gt;
&lt;h2 id="section-1-adding-storage-to-your-server"&gt;Section 1: Adding Storage to Your Server
&lt;/h2&gt;&lt;h3 id="power-off-the-server-and-add-drives"&gt;Power Off the Server and Add Drives
&lt;/h3&gt;&lt;h4 id="step-1-physically-connect-the-storage-drives-to-your-server"&gt;Step 1: Physically Connect the Storage Drive(s) to Your Server
&lt;/h4&gt;&lt;p&gt;Power off command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo shutdown -h now
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;For internal drives, install the drive into a free slot and connect it using SATA power and data cables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For external drives, plug it into a USB port. No need to power down for this one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Power on the server and confirm the drive is recognized.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ssh-into-your-new-server"&gt;SSH Into Your New Server
&lt;/h3&gt;&lt;p&gt;Not sure how to use SSH? Read this post: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/" &gt;Master the Basics - How to SSH Into a Linux Server&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="steps-to-connect-using-ssh"&gt;Steps to Connect Using SSH
&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open PowerShell on Windows&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The command for SSH:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Powershell" data-lang="Powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ssh&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;@&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;server-ip&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example From Part 1:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-PowerShell" data-lang="PowerShell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ssh&lt;/span&gt; &lt;span class="n"&gt;kryptikwurm&lt;/span&gt;&lt;span class="nv"&gt;@192&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;168&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="4"&gt;
&lt;li&gt;&lt;strong&gt;Authenticate&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Enter your server password when prompted. &lt;em&gt;(Don&amp;rsquo;t worry if you don&amp;rsquo;t see any characters while typing. That&amp;rsquo;s normal for security reasons.)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="partition-and-format-the-new-drive"&gt;Partition and Format the New Drive
&lt;/h3&gt;&lt;h4 id="step-2-check-if-the-drive-is-detected"&gt;Step 2: Check if the Drive is Detected
&lt;/h4&gt;&lt;p&gt;Use the &lt;code&gt;lsblk&lt;/code&gt; command to list all block (storage) devices:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsblk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Identify the new drive (e.g., &lt;code&gt;/dev/sdb&lt;/code&gt; or &lt;code&gt;/dev/sdc&lt;/code&gt;) based on its size.&lt;/p&gt;
&lt;p&gt;Note the name of the device (e.g., &lt;code&gt;/dev/sdb&lt;/code&gt;). You&amp;rsquo;ll use it in the next steps.&lt;/p&gt;
&lt;p&gt;Details on how to partition and format a hard drive in Linux are here: &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-how-to-use-parted-to-create-partitions/" &gt;Master the Basics - How to Use parted to Create Partitions&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="step-3-creating-the-basic-folder-structure-of-your-media-library"&gt;Step 3: Creating the Basic Folder Structure of Your Media Library
&lt;/h4&gt;&lt;p&gt;As explained in &lt;strong&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/" &gt;this post&lt;/a&gt;&lt;/strong&gt; your folder structure for Jellyfin should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; mnt/media/
├── Movies/
├── Shows/
└── Music/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To create these folders in &lt;code&gt;/mnt/media&lt;/code&gt; use this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/media/Movies /mnt/media/Shows /mnt/media/Music
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To verify this worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should look something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; root root &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; root root &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Music
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; root root &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Shows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;root:root&lt;/code&gt; ownership won&amp;rsquo;t work for your normal user. Let&amp;rsquo;s fix that.&lt;/p&gt;
&lt;p&gt;Change ownership of the folders:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R &amp;lt;username&amp;gt;:&amp;lt;username&amp;gt; /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chown -R kryptikwurm:kryptikwurm /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To verify the ownership change worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should now look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Music
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxr-xr-x &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Shows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now grant Read, Write, and Execute to this user:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo chmod -R &lt;span class="m"&gt;770&lt;/span&gt; /mnt/media/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To verify the permission change worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should now look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxrwx--- &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxrwx--- &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Music
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drwxrwx--- &lt;span class="m"&gt;2&lt;/span&gt; kryptikwurm kryptikwurm &lt;span class="m"&gt;4096&lt;/span&gt; Jan &lt;span class="m"&gt;17&lt;/span&gt; 13:47 Shows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="section-2-setting-up-smb-samba-shares"&gt;Section 2: Setting Up SMB (Samba) Shares
&lt;/h2&gt;&lt;h4 id="step-1-install-samba-and-nano"&gt;Step 1: Install Samba and Nano
&lt;/h4&gt;&lt;p&gt;To share folders and files between your Linux server and other devices (including Windows boxes), you need Samba. Think of Samba as the bridge that connects different operating systems so they can swap files without drama.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll also want Nano, a beginner-friendly text editor for Linux. It&amp;rsquo;s perfect for quickly editing config files without fuss.&lt;/p&gt;
&lt;p&gt;To install both Samba and Nano in one step, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install samba nano -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells your server to download and install the required packages automatically. The &lt;code&gt;-y&lt;/code&gt; flag confirms the installation for you.&lt;/p&gt;
&lt;h4 id="step-2-configure-the-samba-share"&gt;&lt;strong&gt;Step 2: Configure the Samba Share&lt;/strong&gt;
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Open the Samba configuration file for editing:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/samba/smb.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Add the following section at the end of the file to define the media share:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;[Media]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; browseable = yes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; read only = no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; guest ok = no
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; valid users = your-username&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save and exit Nano:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+O&lt;/code&gt; to save changes.&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Enter&lt;/code&gt; to confirm.&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+X&lt;/code&gt; to exit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So what does all of this actually do? Let&amp;rsquo;s break it down:&lt;/p&gt;
&lt;h4 id="media"&gt;&lt;code&gt;[Media]&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;This is the name of the share. When you connect from a client device, this is what shows up in the network. Change it to something meaningful for your setup, like &lt;code&gt;[Movies]&lt;/code&gt; or &lt;code&gt;[Files]&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="path--mntmedia"&gt;&lt;code&gt;path = /mnt/media&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;This points to the directory on your server that gets shared. In this example, the folder &lt;code&gt;/mnt/media&lt;/code&gt; is being shared. Replace &lt;code&gt;/mnt/media&lt;/code&gt; with the actual path of the directory you want to share.&lt;/p&gt;
&lt;h4 id="browseable--yes"&gt;&lt;code&gt;browseable = yes&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;Setting this to &lt;code&gt;yes&lt;/code&gt; makes the share visible when users browse the network. Set it to &lt;code&gt;no&lt;/code&gt; and users have to know the share&amp;rsquo;s name to connect manually.&lt;/p&gt;
&lt;h4 id="read-only--no"&gt;&lt;code&gt;read only = no&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;This lets users add, modify, or delete files in the share. Set it to &lt;code&gt;yes&lt;/code&gt; and the share becomes read-only.&lt;/p&gt;
&lt;h4 id="guest-ok--no"&gt;&lt;code&gt;guest ok = no&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;This blocks unauthenticated users (guests) from touching the share. Only users with valid credentials get in.&lt;/p&gt;
&lt;h4 id="valid-users--your-username"&gt;&lt;code&gt;valid users = your-username&lt;/code&gt;
&lt;/h4&gt;&lt;p&gt;This restricts access to the share to the specified username. Replace &lt;code&gt;your-username&lt;/code&gt; with the actual username you set up on the Ubuntu server. Only that account can connect.&lt;/p&gt;
&lt;h4 id="step-3-create-a-samba-user"&gt;Step 3: Create a Samba User
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;Add your server user as a Samba user:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo smbpasswd -a your-username
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Follow the prompts to create a password for the Samba user.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="step-4-restart-samba-service"&gt;Step 4: Restart Samba Service
&lt;/h4&gt;&lt;p&gt;Restart the Samba service to apply the changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl restart smbd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="section-3-accessing-the-smb-share-from-another-device"&gt;Section 3: Accessing the SMB Share from Another Device
&lt;/h2&gt;&lt;h4 id="on-windows"&gt;On Windows
&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;Open File Explorer and type the server&amp;rsquo;s IP address in the address bar, prefixed with &lt;code&gt;\\&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&amp;lt;server-ip&amp;gt;&lt;span class="se"&gt;\&amp;lt;&lt;/span&gt;share-name&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example: &lt;code&gt;\\192.168.1.100\media&lt;/code&gt;&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb_hu_a28a4cb61573d7a.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb_hu_69698a176384dc4e.avif 1181w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb_hu_78b58e1e9398bc11.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb_hu_78b58e1e9398bc11.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb_hu_8f3c5d9cc44af5b4.webp 1181w" width="800" height="150" alt="This PC Explorer" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Enter your Samba username and password when prompted.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb2_hu_1892bcd43fdeba75.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb2_hu_a52711c93611b2a6.webp" width="562" height="833" alt="Windows Authenticate" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ol start="3"&gt;
&lt;li&gt;The shared folder should now be accessible, and you can drag and drop files into it.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb3_hu_ec98908952650c40.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb3_hu_33ce5b54e951ff3f.avif 1383w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb3_hu_cb9e71f4dc601592.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb3_hu_cb9e71f4dc601592.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/windows_smb3_hu_239c4e91f170f85a.webp 1383w" width="800" height="205" alt="The SMB shares within Windows" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h4 id="on-macos-or-linux"&gt;On macOS or Linux
&lt;/h4&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open your file manager and choose &amp;ldquo;Connect to Server&amp;rdquo; (or a similar option).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the SMB address:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;smb://&amp;lt;server-ip&amp;gt;/Media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example: &lt;code&gt;smb://192.168.1.100/Media&lt;/code&gt;.&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Log in with your Samba username and password to access the share.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="conclusion"&gt;Conclusion
&lt;/h3&gt;&lt;p&gt;Adding storage and setting up SMB shares looks intimidating until you&amp;rsquo;ve done it once. By the end of this guide you added a new drive, built a clean media library structure, and shared it across your network with Samba. Your server can now accept files from any machine on your LAN.&lt;/p&gt;
&lt;p&gt;Next, test the share by copying a movie file from your desktop to &lt;code&gt;\\&amp;lt;server-ip&amp;gt;\media\Movies&lt;/code&gt;. If it lands and you can read it back, your storage layer is done. If Windows throws a permission error, recheck the &lt;code&gt;chmod -R 770&lt;/code&gt; and the &lt;code&gt;valid users&lt;/code&gt; line in &lt;code&gt;smb.conf&lt;/code&gt; first. Those are the two settings that bite most people.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Storage is ready. Head to Part 3 of this series to install Jellyfin and start serving your collection.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 1 - Installing Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 3 - Installing Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 4 - Installing Configure Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Jellyfin Folder Structure &amp; File Naming: Auto-Fetch Metadata</title><link>https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/</link><pubDate>Sun, 12 Jan 2025 09:47:56 -0700</pubDate><guid>https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/</guid><description>&lt;img src="https://diymediaserver.com/post/how-proper-organization-helps-jellyfin-automatically-fetch-metadata-and-display-content-correctly/featured_hu_4ca50042838cd808.webp" alt="Featured image of post Jellyfin Folder Structure &amp; File Naming: Auto-Fetch Metadata" /&gt;&lt;h2 id="what-is-jellyfin-folder-structure-and-why-does-it-matter"&gt;What Is Jellyfin Folder Structure and Why Does It Matter?
&lt;/h2&gt;&lt;p&gt;Jellyfin folder structure is the specific way you organize media files so Jellyfin can automatically fetch metadata, display posters, and group content correctly. Getting this wrong can cause blank posters, duplicate movies, and scattered TV episodes. Using the proper folder structure makes everything work automatically, no manual intervention needed.&lt;/p&gt;
&lt;p&gt;Remember: it&amp;rsquo;s almost never Jellyfin&amp;rsquo;s fault. It&amp;rsquo;s your folder structure and file naming.&lt;/p&gt;
&lt;p&gt;I know what you&amp;rsquo;re thinking, &amp;ldquo;But it worked fine in Kodi!&amp;rdquo; or &amp;ldquo;My folders make perfect sense to me!&amp;rdquo; I&amp;rsquo;ve been there too. When I migrated from Kodi to Jellyfin, I thought my organization was solid. It mostly was. But &amp;ldquo;mostly&amp;rdquo; meant duplicate movies, missing shows, and entire seasons that refused to group correctly. Once I fixed the structure and naming, everything fell into place.&lt;/p&gt;
&lt;p&gt;Jellyfin is good at fetching posters, descriptions, cast info, and episode data from TMDB and TheTVDB. But it needs your help. It can&amp;rsquo;t read your mind. It reads your folder names and filenames, then matches them against online databases.&lt;/p&gt;
&lt;p&gt;This guide will show you exactly how to organize your media so Jellyfin&amp;rsquo;s metadata matching works without manual cleanup.&lt;/p&gt;
&lt;h2 id="the-three-rules-that-break-everything"&gt;The Three Rules That Break Everything
&lt;/h2&gt;&lt;p&gt;Before we dig in, three mistakes cause about 90% of metadata problems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Missing release years on movies&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Without the year, Jellyfin&amp;rsquo;s playing a guessing game. &amp;ldquo;The Thing&amp;rdquo; could be the 1982 classic or the 2011 prequel. Guess which one it picks? Usually the wrong one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Wrong season/episode format&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;S01E01&lt;/code&gt;, not &lt;code&gt;1x1&lt;/code&gt; or &lt;code&gt;Season 1 Episode 1&lt;/code&gt; or whatever creative variation you&amp;rsquo;ve got going on. Jellyfin expects a specific format, and anything else confuses it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Mixed content types in one library&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Movies and TV shows need separate libraries. Mix them, and you&amp;rsquo;ll get generic metadata, broken browsing, and a headache.&lt;/p&gt;
&lt;p&gt;Get these three right and most of your problems disappear.&lt;/p&gt;
&lt;h2 id="why-folder-organization-matters-for-jellyfin"&gt;Why Folder Organization Matters for Jellyfin
&lt;/h2&gt;&lt;p&gt;You probably have thousands of files that work fine in your file browser. Why does Jellyfin care how they&amp;rsquo;re named?&lt;/p&gt;
&lt;p&gt;Because Jellyfin doesn&amp;rsquo;t know what your files are. It looks at folder names and filenames, then tries to match them against online metadata providers. When your media organization is correct, Jellyfin can automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download posters and background art&lt;/li&gt;
&lt;li&gt;Group TV episodes into seasons&lt;/li&gt;
&lt;li&gt;Sort movies correctly&lt;/li&gt;
&lt;li&gt;Display accurate titles, summaries, and cast info&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When it&amp;rsquo;s wrong? You get blank posters, duplicate movies, TV episodes listed as individual videos, and missing or mismatched metadata.&lt;/p&gt;
&lt;p&gt;That flat folder with 500 randomly-named movie files might work for you, but it&amp;rsquo;s a nightmare for automated metadata. And honestly, it&amp;rsquo;ll become a nightmare for you too once your library hits a few hundred items.&lt;/p&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;
Large, reliable storage is essential for organizing and storing a well-structured media library that Jellyfin can index and fetch metadata from accurately; however, this drive is best for single-drive or light-duty setups rather than high-availability NAS arrays.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="start-with-the-right-top-level-folder-structure"&gt;Start With the Right Top-Level Folder Structure
&lt;/h2&gt;&lt;p&gt;Before you worry about individual filenames, get your top-level structure right. Jellyfin works best when each media type lives in its own library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here&amp;rsquo;s what I recommend:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/media
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── shows
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── music
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each of these folders should be added to Jellyfin as a &lt;strong&gt;separate library&lt;/strong&gt;, with the correct library type selected. Don&amp;rsquo;t mix movies and TV shows in the same library. I know it seems convenient, but it breaks everything. Mixed content gets generic metadata and kills browsing features.&lt;/p&gt;
&lt;h2 id="how-to-organize-jellyfin-library-movies"&gt;How to Organize Jellyfin Library: Movies
&lt;/h2&gt;&lt;p&gt;Movies need &lt;strong&gt;one movie per folder&lt;/strong&gt;, with the movie name and release year clearly visible. That&amp;rsquo;s it. That&amp;rsquo;s the secret.&lt;/p&gt;
&lt;h3 id="the-correct-movie-folder-structure"&gt;The Correct Movie Folder Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/movies
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Inception (2010)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Inception (2010).mkv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Inception (2010).srt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── poster.jpg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Critical rules:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One folder per movie&lt;/li&gt;
&lt;li&gt;Include the release year in parentheses&lt;/li&gt;
&lt;li&gt;Movie file name should match the folder name&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-the-year-matters"&gt;Why the Year Matters
&lt;/h3&gt;&lt;p&gt;Remember &amp;ldquo;The Thing&amp;rdquo; example? Without the year, Jellyfin picks one version, often the wrong one. &amp;ldquo;The Thing (1982)&amp;rdquo; eliminates the guesswork entirely. Same goes for &amp;ldquo;True Grit&amp;rdquo; (1969 vs 2010), &amp;ldquo;Halloween&amp;rdquo; (1978 vs 2018), and dozens of other remakes.&lt;/p&gt;
&lt;p&gt;Five seconds adding a year saves you ten minutes of manual metadata fixing later.&lt;/p&gt;
&lt;h3 id="extras-and-bonus-content"&gt;Extras and Bonus Content
&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;ve got deleted scenes or behind-the-scenes content, Jellyfin supports that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Inception (2010)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Inception (2010).mkv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Extras
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Behind the Scenes.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you open the movie in Jellyfin, you&amp;rsquo;ll see an Extras section populated automatically.&lt;/p&gt;
&lt;h3 id="what-about-deep-folder-structures"&gt;What About Deep Folder Structures?
&lt;/h3&gt;&lt;p&gt;Some people organize like &lt;code&gt;/movies/Christopher Nolan/Inception/&lt;/code&gt;. It can work if everything&amp;rsquo;s named perfectly, but honestly? It adds complexity without much benefit. The &lt;a class="link" href="https://jellyfin.org/docs/general/server/media/movies" target="_blank" rel="noopener"
&gt;official Jellyfin documentation&lt;/a&gt; recommends keeping movies directly under the movies root for reliability.&lt;/p&gt;
&lt;div class="product-box" data-asin="B08146GB6Y"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_428566aaa6c6d13a.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/define-7-xl-45deg_hu_d5473271928309ea.webp" width="600" height="758" alt="Fractal Design Define 7 XL" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;Fractal Design Define 7 XL&lt;/strong&gt;
A quiet full-tower keeps a homelab or media server tidy, with enough drive room that the library can grow into the folder structure this guide recommends. It&amp;rsquo;s overkill for a small or prebuilt system.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3GG6cLC" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="tv-show-folder-structure-for-jellyfin"&gt;TV Show Folder Structure for Jellyfin
&lt;/h2&gt;&lt;p&gt;TV shows are where things get finicky. You need season folders and strict episode naming.&lt;/p&gt;
&lt;h3 id="the-correct-tv-show-structure"&gt;The Correct TV Show Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/shows
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Breaking Bad
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── Season 01
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── Breaking Bad - S01E01.mkv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── Breaking Bad - S01E02.mkv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Season 02
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Breaking Bad - S02E01.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="episode-naming-rules"&gt;Episode Naming Rules
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;SXXEYY&lt;/code&gt; format for episodes&lt;/li&gt;
&lt;li&gt;Always use leading zeros: &lt;code&gt;S01&lt;/code&gt;, not &lt;code&gt;S1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Episode numbers matter more than episode titles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can include episode titles if you want (&lt;code&gt;Breaking Bad - S01E01 - Pilot.mkv&lt;/code&gt;), but the &lt;code&gt;S01E01&lt;/code&gt; part is what Jellyfin parses for matching.&lt;/p&gt;
&lt;h3 id="multi-episode-files"&gt;Multi-Episode Files
&lt;/h3&gt;&lt;p&gt;Got one file that contains multiple episodes? No problem:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Breaking Bad - S01E01-E02.mkv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Jellyfin understands this and will split the metadata correctly. You&amp;rsquo;ll see two episodes in the interface, both pointing to the same file.&lt;/p&gt;
&lt;h2 id="music-folder-organization"&gt;Music Folder Organization
&lt;/h2&gt;&lt;p&gt;Music works differently. &lt;strong&gt;Embedded metadata matters more than filenames&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="recommended-music-structure"&gt;Recommended Music Structure
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/music
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Daft Punk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Random Access Memories
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 01 - Give Life Back to Music.flac
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 02 - The Game of Love.flac
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 03 - Giorgio by Moroder.flac
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Artist and album folders help with browsing, but embedded tags (artist, album, track number) drive the metadata. If your music metadata is messy, use a tag editor like &lt;a class="link" href="https://picard.musicbrainz.org/" target="_blank" rel="noopener"
&gt;MusicBrainz Picard&lt;/a&gt; before importing. Jellyfin can&amp;rsquo;t fix bad tags. It only displays what&amp;rsquo;s already in the file.&lt;/p&gt;
&lt;h2 id="adding-your-media-to-jellyfin-libraries"&gt;Adding Your Media to Jellyfin Libraries
&lt;/h2&gt;&lt;p&gt;Your folders are organized. Now add them to Jellyfin:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the Jellyfin dashboard&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Libraries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Media Library&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select the correct type: Movies, TV Shows, or Music&lt;/li&gt;
&lt;li&gt;Add the matching folder path&lt;/li&gt;
&lt;li&gt;Enable metadata providers (TMDB for movies, TheTVDB for TV)&lt;/li&gt;
&lt;li&gt;Save and scan&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="alert alert-warning"&gt;
&lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;Warning:&lt;/strong&gt;
Double-check that the library type matches the content. A movie library pointed at TV folders will never behave correctly. I&amp;rsquo;ve done this. It&amp;rsquo;s confusing as hell.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When the scan finishes, you should see posters appearing, episode counts looking right, and metadata filling in. If you see that, you&amp;rsquo;re golden.&lt;/p&gt;
&lt;h2 id="scan-verify-and-fix-early"&gt;Scan, Verify, and Fix Early
&lt;/h2&gt;&lt;p&gt;After the first scan, don&amp;rsquo;t assume everything worked. Spot-check a few movies and shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirm posters and summaries appear&lt;/li&gt;
&lt;li&gt;Look for duplicates or missing items&lt;/li&gt;
&lt;li&gt;Check that TV seasons are grouping correctly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I first migrated from Kodi, I assumed my structure was fine. The missing years and inconsistent episode naming caused duplicates and ignored seasons. Fixing filenames solved it way faster than any manual metadata edit would have.&lt;/p&gt;
&lt;p&gt;Catch problems early when you&amp;rsquo;ve got 50 items, not after you&amp;rsquo;ve imported 5,000.&lt;/p&gt;
&lt;h2 id="advanced-tools-and-automation"&gt;Advanced Tools and Automation
&lt;/h2&gt;&lt;p&gt;For large libraries, manual renaming is painful. Tools like Filebot or Sonarr can automate proper naming using the same patterns I&amp;rsquo;ve shown here.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the thing: automation is powerful, but mistakes scale quickly. Always test changes on a small batch first. I once accidentally renamed 200 movies incorrectly because I didn&amp;rsquo;t check the pattern. Don&amp;rsquo;t be like me.&lt;/p&gt;
&lt;div class="product-box" data-asin="B07YP9FBMM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_84a3675c3ec53e96.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_541c32e7cf211fab.webp" width="600" height="284" alt="NVIDIA SHIELD Pro" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;strong&gt;NVIDIA SHIELD Pro&lt;/strong&gt;
Nice to have but not required. A premium streaming client like the SHIELD Pro displays an organized Jellyfin library cleanly and handles advanced playback features, but any capable client will do once your folders are right.
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4gZhtU2" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="troubleshooting-common-jellyfin-organization-issues"&gt;Troubleshooting Common Jellyfin Organization Issues
&lt;/h2&gt;&lt;h3 id="jellyfin-shows-blank-posters"&gt;Jellyfin Shows Blank Posters
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Check the folder and filename format&lt;/li&gt;
&lt;li&gt;Add the release year for movies&lt;/li&gt;
&lt;li&gt;Refresh metadata after fixing names (right-click the item, Refresh Metadata)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="movies-appear-twice"&gt;Movies Appear Twice
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Same movie exists in multiple folders&lt;/li&gt;
&lt;li&gt;Different naming variations creating duplicate matches (like &amp;ldquo;Inception (2010)&amp;rdquo; and &amp;ldquo;Inception&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="tv-episodes-not-grouped-into-seasons"&gt;TV Episodes Not Grouped Into Seasons
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Episode names missing &lt;code&gt;SXXEYY&lt;/code&gt; format&lt;/li&gt;
&lt;li&gt;Season folders incorrectly named or missing&lt;/li&gt;
&lt;li&gt;Episodes placed at the show root instead of season folders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one drove me nuts for a week before I realized I had episodes sitting directly in the show folder instead of in Season folders.&lt;/p&gt;
&lt;h3 id="music-albums-mixed-or-incorrect"&gt;Music Albums Mixed or Incorrect
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Embedded tags are wrong or missing&lt;/li&gt;
&lt;li&gt;Fix tags with a music tagger before rescanning&lt;/li&gt;
&lt;li&gt;Jellyfin can&amp;rsquo;t guess music metadata from filenames alone&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="library-scans-but-nothing-appears"&gt;Library Scans but Nothing Appears
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Wrong library type selected (movies scanning TV content)&lt;/li&gt;
&lt;li&gt;Pointing to the wrong folder path&lt;/li&gt;
&lt;li&gt;File permissions preventing Jellyfin from accessing the files (the Jellyfin process needs read access; on Linux check ownership with &lt;code&gt;ls -ln /media&lt;/code&gt; and confirm the &lt;code&gt;jellyfin&lt;/code&gt; user or your Docker &lt;code&gt;PUID&lt;/code&gt;/&lt;code&gt;PGID&lt;/code&gt; can read the tree)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Check the Jellyfin logs if you&amp;rsquo;re stuck. They&amp;rsquo;ll usually tell you exactly what&amp;rsquo;s wrong. The dashboard exposes them at &lt;strong&gt;Dashboard &amp;gt; Logs&lt;/strong&gt;, and on a typical Linux install they also live in &lt;code&gt;/var/log/jellyfin/&lt;/code&gt; (Docker users: &lt;code&gt;/config/log/&lt;/code&gt; inside the container).&lt;/p&gt;
&lt;h2 id="get-your-jellyfin-folder-structure-right-once"&gt;Get Your Jellyfin Folder Structure Right Once
&lt;/h2&gt;&lt;p&gt;Proper Jellyfin folder structure is the foundation of a working library. When your folders and filenames follow these patterns, metadata fills in on its own. Posters appear, seasons group correctly, and you stop fighting your server.&lt;/p&gt;
&lt;p&gt;Take the time to organize your Jellyfin library correctly once. It saves countless hours of manual fixes later.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re starting fresh, organize first, then scan. Your future self will thank you. And if you&amp;rsquo;ve already got a messy library, it&amp;rsquo;s painful to fix, but it&amp;rsquo;s worth doing before it grows another thousand files.&lt;/p&gt;</description></item><item><title>Master the Basics - How to Use Fdisk to Create Partitions Format Them and Add a Mount Point</title><link>https://diymediaserver.com/post/master-the-basics-how-to-use-fdisk-to-create-partitions-format-them-and-add-a-mount-point-to-fstab/</link><pubDate>Sun, 12 Jan 2025 08:34:30 -0700</pubDate><guid>https://diymediaserver.com/post/master-the-basics-how-to-use-fdisk-to-create-partitions-format-them-and-add-a-mount-point-to-fstab/</guid><description>&lt;img src="https://diymediaserver.com/post/master-the-basics-how-to-use-fdisk-to-create-partitions-format-them-and-add-a-mount-point-to-fstab/fdisk_fstab2_hu_a6b9fa746afefdfb.webp" alt="Featured image of post Master the Basics - How to Use Fdisk to Create Partitions Format Them and Add a Mount Point" /&gt;&lt;p&gt;So you bought that multi-terabyte hard drive and want to bolt it onto your server. You&amp;rsquo;re staring at a fresh disk and you&amp;rsquo;re not sure where to start. I&amp;rsquo;ve got you covered.&lt;/p&gt;
&lt;p&gt;Setting up storage on Linux looks intimidating the first time, especially when &lt;code&gt;fdisk&lt;/code&gt; and &lt;code&gt;fstab&lt;/code&gt; enter the picture. It&amp;rsquo;s not as bad as it looks. By the end of this guide you&amp;rsquo;ll know how to carve up a disk with &lt;code&gt;fdisk&lt;/code&gt;, slap a filesystem on it, and wire it into &lt;code&gt;fstab&lt;/code&gt; so it mounts automatically at boot.&lt;/p&gt;
&lt;h2 id="the-power-of-partitions-and-why-they-matter"&gt;The Power of Partitions (and Why They Matter)
&lt;/h2&gt;&lt;p&gt;Think of your hard drive as a giant empty filing cabinet. Without partitions, your system has no idea how to organize or access any of that space. Partitions break the drive into smaller sections, each with a specific purpose. Data here, programs there, backups over in the corner. That separation is what keeps things sane when something goes sideways.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fdisk&lt;/code&gt; is the tool that creates and manages those partitions. It ships with almost every Linux distribution, so you don&amp;rsquo;t have to install anything. I&amp;rsquo;ll walk you through every step, whether you&amp;rsquo;re setting up a brand-new drive or reorganizing an existing one.&lt;/p&gt;
&lt;h2 id="step-by-step-guide-to-using-fdisk-formatting-partitions-and-configuring-fstab"&gt;Step-by-Step Guide to Using &lt;code&gt;fdisk&lt;/code&gt;, Formatting Partitions, and Configuring &lt;code&gt;fstab&lt;/code&gt;
&lt;/h2&gt;&lt;h3 id="step-1-identify-the-target-disk"&gt;Step 1: Identify the Target Disk
&lt;/h3&gt;&lt;p&gt;Before you touch anything, figure out which drive you&amp;rsquo;re working with. Use &lt;code&gt;lsblk&lt;/code&gt; to list every disk and partition the kernel can see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lsblk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll get something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sda 8:0 &lt;span class="m"&gt;0&lt;/span&gt; 500G &lt;span class="m"&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─sda1 8:1 &lt;span class="m"&gt;0&lt;/span&gt; 1M &lt;span class="m"&gt;0&lt;/span&gt; part
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─sda2 8:2 &lt;span class="m"&gt;0&lt;/span&gt; 2G &lt;span class="m"&gt;0&lt;/span&gt; part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─sda3 8:3 &lt;span class="m"&gt;0&lt;/span&gt; 498G &lt;span class="m"&gt;0&lt;/span&gt; part /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sdb 8:16 &lt;span class="m"&gt;0&lt;/span&gt; 12.7T &lt;span class="m"&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here &lt;code&gt;sda&lt;/code&gt; is your main disk and &lt;code&gt;sdb&lt;/code&gt; is the empty drive. We&amp;rsquo;ll work with &lt;code&gt;sdb&lt;/code&gt; for the rest of this tutorial.&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Caution:&lt;/strong&gt; Double-check the disk name before you do anything destructive. Pick the wrong one and you wipe a drive full of data you wanted to keep.&lt;/p&gt;
&lt;h3 id="step-2-launch-fdisk"&gt;Step 2: Launch &lt;code&gt;fdisk&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Open &lt;code&gt;fdisk&lt;/code&gt; against the target disk:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo fdisk /dev/sdb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see a prompt like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Command (m for help):
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Type &lt;code&gt;m&lt;/code&gt; to see the available commands.&lt;/p&gt;
&lt;h3 id="step-3-create-a-new-partition"&gt;Step 3: Create a New Partition
&lt;/h3&gt;&lt;h4 id="1-delete-old-partitions-optional"&gt;1. Delete Old Partitions (Optional)
&lt;/h4&gt;&lt;p&gt;If the disk already has partitions you want gone, type &lt;code&gt;d&lt;/code&gt; and follow the prompts to delete them.&lt;/p&gt;
&lt;h4 id="2-create-a-new-partition"&gt;2. Create a New Partition
&lt;/h4&gt;&lt;p&gt;To create a new partition, type &lt;code&gt;n&lt;/code&gt; and press &lt;strong&gt;Enter&lt;/strong&gt;. &lt;code&gt;fdisk&lt;/code&gt; will ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Partition type:&lt;/strong&gt; Choose &lt;code&gt;p&lt;/code&gt; for primary or &lt;code&gt;e&lt;/code&gt; for extended. Pick &lt;code&gt;p&lt;/code&gt; for most use cases.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Partition number:&lt;/strong&gt; Press &lt;strong&gt;Enter&lt;/strong&gt; to accept the default (usually 1).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;First sector:&lt;/strong&gt; Press &lt;strong&gt;Enter&lt;/strong&gt; to accept the default (start of the disk).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Last sector:&lt;/strong&gt; Press &lt;strong&gt;Enter&lt;/strong&gt; to use the entire disk, or specify a size like &lt;code&gt;+100G&lt;/code&gt; for a 100GB partition.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you&amp;rsquo;re done you&amp;rsquo;ll see the new partition listed.&lt;/p&gt;
&lt;h4 id="3-write-changes-to-disk"&gt;3. Write Changes to Disk
&lt;/h4&gt;&lt;p&gt;Type &lt;code&gt;w&lt;/code&gt; to save the changes and exit &lt;code&gt;fdisk&lt;/code&gt;. This writes the partition table to the disk. Nothing you&amp;rsquo;ve done so far touches the disk until you hit &lt;code&gt;w&lt;/code&gt;, so if you panicked halfway through you can always quit with &lt;code&gt;q&lt;/code&gt; and walk away.&lt;/p&gt;
&lt;h3 id="step-4-format-the-partition"&gt;Step 4: Format the Partition
&lt;/h3&gt;&lt;p&gt;You&amp;rsquo;ve got a partition, but it has no filesystem on it yet. A common choice is &lt;code&gt;ext4&lt;/code&gt;. For a media server hosting big files, I reach for XFS instead.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkfs.xfs /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That formats the first partition on &lt;code&gt;sdb&lt;/code&gt; with XFS. Swap &lt;code&gt;xfs&lt;/code&gt; for &lt;code&gt;ext4&lt;/code&gt; or &lt;code&gt;btrfs&lt;/code&gt; if you&amp;rsquo;d rather use those.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;lsblk&lt;/code&gt; again and the output should look similar to this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sda 8:0 &lt;span class="m"&gt;0&lt;/span&gt; 500G &lt;span class="m"&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─sda1 8:1 &lt;span class="m"&gt;0&lt;/span&gt; 1M &lt;span class="m"&gt;0&lt;/span&gt; part
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─sda2 8:2 &lt;span class="m"&gt;0&lt;/span&gt; 2G &lt;span class="m"&gt;0&lt;/span&gt; part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─sda3 8:3 &lt;span class="m"&gt;0&lt;/span&gt; 498G &lt;span class="m"&gt;0&lt;/span&gt; part /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sdb 8:16 &lt;span class="m"&gt;0&lt;/span&gt; 12.7T &lt;span class="m"&gt;0&lt;/span&gt; disk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─sdb1 8:17 &lt;span class="m"&gt;0&lt;/span&gt; 12.7T &lt;span class="m"&gt;0&lt;/span&gt; part
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="step-5-create-a-mount-point"&gt;Step 5: Create a Mount Point
&lt;/h3&gt;&lt;p&gt;A mount point is where the system will hang the partition in the filesystem tree. For a media drive, something like this works:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mkdir -p /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; flag tells &lt;code&gt;mkdir&lt;/code&gt; to create any parent directories that don&amp;rsquo;t exist yet.&lt;/p&gt;
&lt;h3 id="step-6-mount-the-partition-temporary-test"&gt;Step 6: Mount the Partition (Temporary Test)
&lt;/h3&gt;&lt;p&gt;Before you commit anything to &lt;code&gt;fstab&lt;/code&gt;, test the mount by hand:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount /dev/sdb1 /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Verify it&amp;rsquo;s mounted:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should see &lt;code&gt;/dev/sdb1&lt;/code&gt; listed with &lt;code&gt;/mnt/media&lt;/code&gt; as its mount point. If you don&amp;rsquo;t, stop here and figure out why before you touch &lt;code&gt;fstab&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="step-7-add-the-partition-to-fstab"&gt;Step 7: Add the Partition to &lt;code&gt;fstab&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;Now you want this thing to come back automatically after a reboot. That&amp;rsquo;s what &lt;code&gt;/etc/fstab&lt;/code&gt; is for. Start by grabbing the partition&amp;rsquo;s UUID:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo blkid /dev/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see output like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/dev/sdb1: UUID=&amp;#34;1234-5678-90AB-CDEF&amp;#34; TYPE=&amp;#34;xfs&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Copy that UUID and open &lt;code&gt;fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nano /etc/fstab
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Add this line at the end of the file:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;UUID=1234-5678-90AB-CDEF /mnt/media xfs defaults 0 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s what each field does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;UUID=1234-5678-90AB-CDEF&lt;/strong&gt;: Unique identifier for the partition. Survives drive reordering, unlike &lt;code&gt;/dev/sdb1&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;/mnt/media&lt;/strong&gt;: Mount point.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;xfs&lt;/strong&gt;: Filesystem type.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;defaults&lt;/strong&gt;: Standard mount options.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;0 2&lt;/strong&gt;: Dump and fsck options. Safe to leave as-is for a data drive.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Save the file and exit.&lt;/p&gt;
&lt;h3 id="step-8-test-the-fstab-configuration"&gt;Step 8: Test the &lt;code&gt;fstab&lt;/code&gt; Configuration
&lt;/h3&gt;&lt;p&gt;Now verify your changes work without rebooting. Unmount the partition, then remount everything in &lt;code&gt;fstab&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo umount /mnt/media
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;mount -a&lt;/code&gt; returns clean with no errors, your configuration is good. If it throws an error, fix the &lt;code&gt;fstab&lt;/code&gt; line before you reboot. A broken &lt;code&gt;fstab&lt;/code&gt; can drop the system into emergency mode on the next boot, and digging your way out of that is no fun.&lt;/p&gt;
&lt;h2 id="wrapping-it-all-up"&gt;Wrapping It All Up
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve carved up a disk, formatted it, and wired it into &lt;code&gt;fstab&lt;/code&gt; so it mounts on every boot. That same pattern works for media storage, backup targets, Docker volume directories, or anywhere else you need a dedicated chunk of disk.&lt;/p&gt;
&lt;p&gt;One last warning. Always double-check the disk name in &lt;code&gt;lsblk&lt;/code&gt; before you run &lt;code&gt;fdisk&lt;/code&gt; or &lt;code&gt;mkfs&lt;/code&gt;, and back up anything important before you reorganize a drive that already has data on it. The commands here don&amp;rsquo;t ask for confirmation, and there&amp;rsquo;s no undo.&lt;/p&gt;</description></item><item><title>Master the Basics - How to SSH Into a Linux Server</title><link>https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/</link><pubDate>Sun, 12 Jan 2025 07:33:02 -0700</pubDate><guid>https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/SSH2_hu_241954f6b52fa638.webp" alt="Featured image of post Master the Basics - How to SSH Into a Linux Server" /&gt;
&lt;div class="alert alert-tldr"&gt;
&lt;span class="alert-icon"&gt;💭&lt;/span&gt;
&lt;div class="alert-content"&gt;
&lt;strong&gt;TL;DR:&lt;/strong&gt;
SSH lets you control your Linux server from any computer on your local network. Install an SSH client, run &lt;code&gt;ssh user@server-ip&lt;/code&gt;, and you&amp;rsquo;re in. Keep it off the public internet.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Do you need to keep a monitor and keyboard hooked up to your server? Nope. SSH lets you reach your server&amp;rsquo;s command prompt from your Windows, Mac, or Linux box. You&amp;rsquo;ve probably seen SSH thrown around in forums and tutorials. It&amp;rsquo;s the tool that lets you run your Linux server from anywhere on your local network.&lt;/p&gt;
&lt;p&gt;One rule first. &lt;strong&gt;Do not expose your SSH port to the internet&lt;/strong&gt; unless you know exactly what you&amp;rsquo;re doing and have a good reason. Bots scan for open port 22 every minute of the day. Keep it inside your LAN.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been running SSH on my own home media server for years. Here&amp;rsquo;s how to set it up without shooting yourself in the foot.&lt;/p&gt;
&lt;h2 id="what-is-ssh-and-why-should-you-care"&gt;What Is SSH, and Why Should You Care?
&lt;/h2&gt;&lt;p&gt;SSH stands for &lt;strong&gt;Secure Shell&lt;/strong&gt;. It&amp;rsquo;s an encrypted protocol that lets you log into a remote machine over the network and run commands as if you were sitting at the keyboard. Want to manage your server from the couch with a laptop on your lap? That&amp;rsquo;s SSH.&lt;/p&gt;
&lt;p&gt;With an SSH session you can run commands, edit configs, copy files with &lt;code&gt;scp&lt;/code&gt; or &lt;code&gt;rsync&lt;/code&gt;, and troubleshoot the box without ever plugging in a monitor. For a home media server, it&amp;rsquo;s the difference between a project and a chore.&lt;/p&gt;
&lt;h2 id="why-you-shouldnt-expose-ssh-to-the-internet"&gt;Why You Shouldn&amp;rsquo;t Expose SSH to the Internet
&lt;/h2&gt;&lt;p&gt;Quick security detour before the how-to. Putting your SSH server on the public internet is asking for trouble. Bots and opportunistic attackers scan the entire IPv4 space for open port 22 around the clock. Find your host, and they&amp;rsquo;ll start brute-forcing usernames and passwords until something gives.&lt;/p&gt;
&lt;p&gt;Three reasons to keep SSH on your LAN only:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reduced Attack Surface&lt;/strong&gt;: Only devices already on your home network can even attempt a connection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Less Config Pain&lt;/strong&gt;: No firewall holes, no fail2ban rules, no key-only enforcement to maintain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Port Forwarding or VPN&lt;/strong&gt;: You don&amp;rsquo;t need to stand up WireGuard or punch holes in your router for a single shell session.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you genuinely need remote access from outside your house, put SSH behind a VPN like WireGuard or Tailscale. Don&amp;rsquo;t forward port 22 and hope for the best.&lt;/p&gt;
&lt;h2 id="step-by-step-guide-to-ssh-into-your-linux-server"&gt;Step-by-Step Guide to SSH into Your Linux Server
&lt;/h2&gt;&lt;h3 id="1-check-your-servers-ip-address"&gt;1. Check Your Server&amp;rsquo;s IP Address
&lt;/h3&gt;&lt;p&gt;You need the server&amp;rsquo;s local IP before you can connect. Log in at the console and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ip addr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look for the &lt;code&gt;inet&lt;/code&gt; line under your active network interface (usually &lt;code&gt;eth0&lt;/code&gt;, &lt;code&gt;ens18&lt;/code&gt;, or similar). The address will look like &lt;code&gt;192.168.x.x&lt;/code&gt; or &lt;code&gt;10.0.x.x&lt;/code&gt;. That&amp;rsquo;s the one you want.&lt;/p&gt;
&lt;p&gt;Example:
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/ipaddress_hu_5285216830897632.avif 800w, https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/ipaddress_hu_380e137b5bf76293.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/ipaddress_hu_b48d80a79a9cd07.webp" srcset="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/ipaddress_hu_b48d80a79a9cd07.webp 800w, https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/ipaddress_hu_ce88565d1306a43d.webp 1600w" width="800" height="219" alt="Old computer to Server" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id="2-install-an-ssh-client-on-your-computer"&gt;2. Install an SSH Client on Your Computer
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows&lt;/strong&gt;: Use the built-in OpenSSH client in PowerShell, or grab &lt;a class="link" href="https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html" target="_blank" rel="noopener"
&gt;PuTTY&lt;/a&gt; if you prefer a GUI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mac/Linux&lt;/strong&gt;: Your terminal already has &lt;code&gt;ssh&lt;/code&gt; installed. Open it and skip ahead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-connect-to-the-server-via-powershell"&gt;3. Connect to the Server via PowerShell
&lt;/h3&gt;&lt;p&gt;On Windows, hit the Start key, type &lt;code&gt;PowerShell&lt;/code&gt;, and click &lt;strong&gt;Windows PowerShell&lt;/strong&gt;.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/powershell_hu_7ea882923992eee8.avif 800w, https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/powershell_hu_ffbecc7e60622474.avif 815w"&gt;&lt;img src="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/powershell_hu_3122e445f8033860.webp" srcset="https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/powershell_hu_3122e445f8033860.webp 800w, https://diymediaserver.com/post/how-to-ssh-into-a-linux-server/powershell_hu_30df5ca6711d484c.webp 815w" width="800" height="487" alt="PowerShell in the Start Menu" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh username@192.168.x.x
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Swap &lt;code&gt;username&lt;/code&gt; for your Linux account and &lt;code&gt;192.168.x.x&lt;/code&gt; for your server&amp;rsquo;s IP.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh john@192.168.1.100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;First connection? You&amp;rsquo;ll see a prompt asking you to verify the server&amp;rsquo;s fingerprint. Type &lt;code&gt;yes&lt;/code&gt; and hit enter.&lt;/p&gt;
&lt;h3 id="4-enter-your-password"&gt;4. Enter Your Password
&lt;/h3&gt;&lt;p&gt;The server prompts for your password next. Type it in carefully. The terminal won&amp;rsquo;t echo characters as you type, which is normal and intentional. Hit enter.&lt;/p&gt;
&lt;p&gt;If the connection works, you&amp;rsquo;ll land at a shell prompt like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;KryptikWurm@mediaserver:~$
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;re in. Run &lt;code&gt;whoami&lt;/code&gt; or &lt;code&gt;hostname&lt;/code&gt; to confirm.&lt;/p&gt;
&lt;h3 id="5-troubleshooting-common-issues"&gt;5. Troubleshooting Common Issues
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;Connection Refused&amp;rdquo;&lt;/strong&gt;: The SSH service isn&amp;rsquo;t running on the server. On the server&amp;rsquo;s console run:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl start ssh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On some distros the unit is named &lt;code&gt;sshd&lt;/code&gt; instead, so try &lt;code&gt;sudo systemctl start sshd&lt;/code&gt; if the first one fails. To make it survive reboots: &lt;code&gt;sudo systemctl enable ssh&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;Permission Denied&amp;rdquo;&lt;/strong&gt;: Wrong username or wrong password. Double-check both. Linux usernames are case-sensitive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;No Route to Host&amp;rdquo; or hangs forever&lt;/strong&gt;: Server and client aren&amp;rsquo;t on the same subnet, or a host firewall is blocking port 22. Check &lt;code&gt;sudo ufw status&lt;/code&gt; on Ubuntu or &lt;code&gt;sudo firewall-cmd --list-all&lt;/code&gt; on Fedora/Rocky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can&amp;rsquo;t find the IP at all&lt;/strong&gt;: Confirm the server is on your LAN and not isolated on a guest VLAN.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="frequently-asked-questions"&gt;Frequently Asked Questions
&lt;/h2&gt;&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ What port does SSH use by default?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;SSH listens on TCP port 22 by default. You can change it in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; by editing the &lt;code&gt;Port&lt;/code&gt; line and restarting the service with &lt;code&gt;sudo systemctl restart ssh&lt;/code&gt;. Changing the port doesn&amp;rsquo;t make SSH meaningfully more secure, it only cuts down on log noise from drive-by bots.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Should I use SSH keys instead of a password?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Yes, especially if SSH will ever leave your LAN. Generate a key on the client with &lt;code&gt;ssh-keygen -t ed25519&lt;/code&gt;, copy it to the server with &lt;code&gt;ssh-copy-id user@server-ip&lt;/code&gt;, and then disable password login in &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; by setting &lt;code&gt;PasswordAuthentication no&lt;/code&gt;. Restart sshd and you&amp;rsquo;re key-only.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ How do I copy files over SSH?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;Use &lt;code&gt;scp&lt;/code&gt; for one-off file transfers: &lt;code&gt;scp localfile user@server-ip:/remote/path/&lt;/code&gt;. For directory sync, &lt;code&gt;rsync -avz localdir/ user@server-ip:/remote/path/&lt;/code&gt; is faster and handles interruptions. Both run over the same SSH connection, so no extra setup is needed.&lt;/div&gt;
&lt;/details&gt;
&lt;details class="collapse md" &gt;
&lt;summary&gt;➤ Why does the terminal not show my password as I type it?&lt;/summary&gt;
&lt;div class="collapse-content"&gt;That&amp;rsquo;s deliberate. SSH suppresses the echo so someone glancing over your shoulder can&amp;rsquo;t count characters or guess the length. Type the password as normal and hit enter. It&amp;rsquo;s still being entered, you can&amp;rsquo;t see it.&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="keep-it-local-keep-it-safe"&gt;Keep It Local, Keep It Safe
&lt;/h2&gt;&lt;p&gt;SSH is a power tool. Keep it on your LAN and you get every benefit of remote management with almost none of the risk. Open it up to the internet without a VPN and you&amp;rsquo;re putting your server on a list of free targets.&lt;/p&gt;
&lt;p&gt;Open a terminal, run &lt;code&gt;ssh user@your-server-ip&lt;/code&gt;, and start working your media server from wherever you sit in the house. If you ever do need outside access, put it behind WireGuard or Tailscale first. Safety third.&lt;/p&gt;</description></item><item><title>Turn an Old Computer Into a Media Server Part 1 Hardware and Installing Linux</title><link>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/</link><pubDate>Fri, 10 Jan 2025 09:07:02 -0700</pubDate><guid>https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/</guid><description>&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/oldpc2_hu_cb6096cf69770cd9.webp" alt="Featured image of post Turn an Old Computer Into a Media Server Part 1 Hardware and Installing Linux" /&gt;&lt;p&gt;Got an old computer collecting dust in the closet? The one you swore you&amp;rsquo;d find a use for someday? Today&amp;rsquo;s the day. Instead of dropping hundreds (or thousands) on shiny new hardware, you can turn that machine into a working media server. It&amp;rsquo;s a cheap way to learn the ropes of home media servers before you commit real money.&lt;/p&gt;
&lt;p&gt;And the best part? You don&amp;rsquo;t need much to get going. A little time and some free software, and you&amp;rsquo;ll be streaming your movies, shows, and music from that old box. Here&amp;rsquo;s what you need and how to wire it up.&lt;/p&gt;
&lt;h2 id="why-start-with-an-old-computer"&gt;Why Start With an Old Computer?
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one rule when you&amp;rsquo;re new to home media servers: start small, upgrade later. The common mistake is jumping straight into an expensive custom build, then realizing the hobby isn&amp;rsquo;t what you thought it would be. An old computer is a low-risk way to learn the basics and find out if this is your thing.&lt;/p&gt;
&lt;p&gt;Think of it as training wheels. You learn how to install software, organize your media, and troubleshoot the inevitable weirdness, all without burning cash on high-end hardware. And it&amp;rsquo;s eco-friendly. That old PC gets a second life instead of a trip to the e-waste pile.&lt;/p&gt;
&lt;h2 id="minimum-specifications-what-does-your-old-computer-need"&gt;Minimum Specifications: What Does Your Old Computer Need?
&lt;/h2&gt;&lt;p&gt;You don&amp;rsquo;t need a gaming rig to run a basic Ubuntu-based Jellyfin server. Ubuntu Server and Jellyfin are both lightweight, so your old machine can probably handle it. Here&amp;rsquo;s the bare minimum to get started.&lt;/p&gt;
&lt;h3 id="bare-minimum-hardware-requirements"&gt;Bare Minimum Hardware Requirements
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Processor (CPU):&lt;/strong&gt; Intel Core i3 2nd Gen or AMD equivalent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 4 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; At least 100 GB of free space (more if you plan to store lots of media locally).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operating System:&lt;/strong&gt; Ubuntu Server 24.04 LTS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;USB Stick:&lt;/strong&gt; 32 GB for the OS installation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network:&lt;/strong&gt; An Ethernet connection (wired or Wi-Fi)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s enough to get a Jellyfin server on its feet. As your library grows or you want extra features, you can upgrade later.&lt;/p&gt;
&lt;h3 id="best-experience-recommended-hardware"&gt;Best Experience Recommended Hardware
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Processor:&lt;/strong&gt; No change&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 8 GB&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OS Storage:&lt;/strong&gt; 200 GB of SSD storage
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B01N5IB20Q"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/kingston-a400-240gb_hu_759a8908d03853e2.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/kingston-a400-240gb_hu_961bafbd9127d7db.webp" width="600" height="420" alt="Kingston 240GB SSD" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Kingston 240GB SSD&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A cheap, dependable SATA SSD for a boot drive. 240GB is plenty for a Linux install plus Docker images and leaves the spinning disks free to do nothing but hold media. Not fast by NVMe standards and not what you want for a cache tier, but it is the standard answer for getting an old desktop booting off flash instead of a tired hard drive.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/40c5wUs" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Media Storage:&lt;/strong&gt; 8 TB or more depending on the size of your media collection
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Network:&lt;/strong&gt; A wired ethernet connection is best&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your computer meets these specs, you&amp;rsquo;re ready. If not, you can probably still make it work with a few tweaks, especially if you&amp;rsquo;re not asking it to transcode video on the fly. Transcoding gets its own post later in the series.&lt;/p&gt;
&lt;h3 id="proof-old-computers-will-work"&gt;Proof Old Computers Will Work
&lt;/h3&gt;&lt;p&gt;To show you this works on real, old hardware, I&amp;rsquo;m building a media server with these parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Processor:&lt;/strong&gt; i5-2500k. This CPU shipped in 2011.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 16 GB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS Storage:&lt;/strong&gt; 500 GB SSD&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Media Storage:&lt;/strong&gt; Two 2 TB Hard Drives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Video Card:&lt;/strong&gt; NVIDIA 750ti (only needed for the install)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So you&amp;rsquo;ve found a candidate machine. Now it&amp;rsquo;s time to turn it into a media server by installing &lt;strong&gt;Ubuntu Server 24.04 LTS&lt;/strong&gt;. This release is built for secure, efficient server work, and it runs fine on older hardware. Here&amp;rsquo;s how to get it installed and out of your way.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;And yes, Debian or any other mainstream Linux distro will work. I picked Ubuntu because it&amp;rsquo;s popular, well-documented, and you&amp;rsquo;ll find an answer to almost any question with one search.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="installing-ubuntu-server-2404-lts"&gt;Installing Ubuntu Server 24.04 LTS
&lt;/h2&gt;&lt;p&gt;Time for the fun part. Follow these steps to get Ubuntu 24.04 installed and running.&lt;/p&gt;
&lt;h3 id="1-prepare-your-old-computer-and-thumb-drive"&gt;1. &lt;strong&gt;Prepare Your Old Computer and Thumb Drive&lt;/strong&gt;
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Hook up a monitor, keyboard, and mouse for setup.&lt;/li&gt;
&lt;li&gt;Download the Ubuntu Server 24.04 LTS ISO from &lt;a class="link" href="https://ubuntu.com/download/server" target="_blank" rel="noopener"
&gt;Ubuntu.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;a class="link" href="https://rufus.ie/en/" target="_blank" rel="noopener"
&gt;Rufus&lt;/a&gt; (on Windows) or &lt;a class="link" href="https://etcher.balena.io/#download-etcher" target="_blank" rel="noopener"
&gt;Etcher&lt;/a&gt; (on macOS/Linux) to write a bootable USB drive with the ISO.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-use-rufus-to-create-a-bootable-thumb-drive"&gt;2. Use Rufus to Create a Bootable Thumb Drive
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Download the portable version of Rufus
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_download_hu_abcf3f22a715dcb6.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_download_hu_98aa0ffb800e690e.avif 942w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_download_hu_8136fb318fd419aa.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_download_hu_8136fb318fd419aa.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_download_hu_8435dcfe172cf8f5.webp 942w" width="800" height="369" alt="Rufus Download" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open Rufus
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_interface_hu_709831b2a175abc4.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_interface_hu_676942c0b34e16cb.webp" width="577" height="701" alt="Rufus Interface" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press SELECT to pick the Ubuntu Server ISO you downloaded, then press START
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_ISO_hu_dc7820d155c1b3f6.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_ISO_hu_10b14c69f0b292f.webp" width="567" height="842" alt="Rufus Select ISO" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When prompted, write in DD Image mode
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_dd_mode_hu_b3fedcadc07510cb.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_dd_mode_hu_64516067c9c7596d.webp" width="633" height="358" alt="Rufus DD Mode" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press OK to format the USB stick&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;This wipes everything on the drive.&lt;/strong&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_format_hu_2996f5885a56619e.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/rufus_format_hu_25c825702f7b9df0.webp" width="547" height="220" alt="Rufus Format Warning" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You now have a bootable USB stick with Ubuntu Server on it.&lt;/p&gt;
&lt;h3 id="3-boot-from-usb"&gt;3. Boot from USB
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Insert the bootable USB into the computer you&amp;rsquo;re installing Ubuntu on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Power on and enter the BIOS/UEFI settings. The hotkey is usually &lt;strong&gt;F2&lt;/strong&gt;, &lt;strong&gt;F12&lt;/strong&gt;, &lt;strong&gt;Del&lt;/strong&gt;, or &lt;strong&gt;Esc&lt;/strong&gt; during the splash screen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the boot order so USB comes before the hard drive. That way, if a USB is present, the system boots from it first.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save and exit. The system reboots into the Ubuntu Server installer.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-begin-the-installation"&gt;4. Begin the Installation
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;GNU Grub Menu. When prompted, press enter.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_grub_install_hu_c7ea2e2bc1a3490.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_grub_install_hu_f2511b61d3d43cc9.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_grub_install_hu_4b3d40aae6cab9d0.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_grub_install_hu_4b3d40aae6cab9d0.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_grub_install_hu_d08e0ebf6e710991.webp 1600w" width="800" height="416" alt="Ubuntu Grub Menu" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select your default language
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_language_install_hu_d30e7ce91bd394b2.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_language_install_hu_12262e4e3aa54be4.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_language_install_hu_219b77079e51bf.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_language_install_hu_219b77079e51bf.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_language_install_hu_d0883573b8a02c21.webp 1600w" width="800" height="404" alt="Ubuntu Select Language" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select your keyboard layout
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_keyboard_install_hu_3e11fe8dbe227b77.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_keyboard_install_hu_65be14a4a5632bf1.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_keyboard_install_hu_61f2af4ef19e5632.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_keyboard_install_hu_61f2af4ef19e5632.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_keyboard_install_hu_688e54f795d23b11.webp 1600w" width="800" height="161" alt="Ubuntu Select Keyboard Layout" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pick the install type. Select Ubuntu Server (minimized) and &amp;ldquo;Search for third-party drivers&amp;rdquo;.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_type_install_hu_6fd8b544093c70c9.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_type_install_hu_ecf8ffe5606a84db.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_type_install_hu_cfc3abd5f68d9ed6.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_type_install_hu_cfc3abd5f68d9ed6.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_type_install_hu_533f26acc3cb37b9.webp 1600w" width="800" height="259" alt="Ubuntu Install Type" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Network Config. This is a server, so it needs a static IP (one that doesn&amp;rsquo;t change). Tab up to the network device name (yours will be different) and press enter.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network1_install_hu_bb1349ffaf81761d.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network1_install_hu_248910f51b34d8d1.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network1_install_hu_65ebdabece251440.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network1_install_hu_65ebdabece251440.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network1_install_hu_1ade50cebfd28f8d.webp 1600w" width="800" height="108" alt="Ubuntu Configure Network" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You&amp;rsquo;ll see this menu. Select Edit IPv4 and press enter.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network2_install_hu_d7c832ad6462760e.avif"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network2_install_hu_f3f94a180bf654a5.webp" width="332" height="206" alt="Ubuntu Network Type" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select Manual from the menu and press enter
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network3_install_hu_4dec65c9e2135f06.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network3_install_hu_607053067fa77a81.avif 1196w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network3_install_hu_1bc402a878f9121e.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network3_install_hu_1bc402a878f9121e.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network3_install_hu_5fc4e18bc48581a3.webp 1196w" width="800" height="185" alt="Ubuntu Network Manual" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;My network config will be different from yours. Chances are your network is on the 192.168.0.0/24 or 192.168.1.0/24 subnet. If you don&amp;rsquo;t know what your network is, open PowerShell on Windows and run:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-Powershell" data-lang="Powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt; &lt;span class="n"&gt;-all&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;ipconfig&lt;/code&gt; output gives you everything you need for the IPv4 config page.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;One thing changes between Windows and Linux: the subnet format. Windows shows 255.255.255.0. Linux wants CIDR. Take your IP, change the last octet to 0, and tack on /24, like this:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.0.0/24
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;or
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.1.0/24
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network4_install_hu_319870bf7a919226.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network4_install_hu_13c225056f0bd1d9.avif 1192w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network4_install_hu_7093b7d3824b5124.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network4_install_hu_7093b7d3824b5124.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_network4_install_hu_9125c1fdb0b2387.webp 1192w" width="800" height="513" alt="Ubuntu Network Config" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Proxy Config. Leave this blank unless you know you&amp;rsquo;re behind a proxy.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_proxy_install_hu_1276f62830d0f74.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_proxy_install_hu_d051234030b60805.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_proxy_install_hu_bb4693d7208f4100.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_proxy_install_hu_bb4693d7208f4100.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_proxy_install_hu_5f612a8a89e99ab6.webp 1600w" width="800" height="123" alt="Ubuntu Select Proxy" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Guided Storage Configuration. For this first setup, leave everything default. I&amp;rsquo;ll cover a more advanced storage layout in another post.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage_install_hu_76974637795b9138.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage_install_hu_ab51e3294996a031.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage_install_hu_4249c3c4dae4a74a.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage_install_hu_4249c3c4dae4a74a.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage_install_hu_65d31a0f7319dbbb.webp 1600w" width="800" height="301" alt="Ubuntu Storage Config" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Storage Config Summary
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage2_install_hu_75fdc7dec57119e7.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage2_install_hu_fbe939e5599dc0e5.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage2_install_hu_5f74ceb9f7638e98.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage2_install_hu_5f74ceb9f7638e98.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage2_install_hu_33996b1707eea946.webp 1600w" width="800" height="387" alt="Ubuntu Storage Summary" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Confirm Destructive Action. This wipes everything from the selected drive.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage3_install_hu_15cf62f9c3e43276.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage3_install_hu_43681faee2fa5246.avif 1193w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage3_install_hu_4ea0c8bba2c34424.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage3_install_hu_4ea0c8bba2c34424.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_storage3_install_hu_dbb0c015db318b33.webp 1193w" width="800" height="290" alt="Ubuntu Destruction Warning" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Profile Configuration&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Your name. Not the username. I make my username and name match.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Your server name. What do you want to call your server?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pick a username. I use what I put in the Your name block.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Password. Make this a strong one.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_user_install_hu_a5a0633c08258c1.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_user_install_hu_c64c37a51aba6526.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_user_install_hu_57e9a8fb1c3f43f.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_user_install_hu_57e9a8fb1c3f43f.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_user_install_hu_e14717fb814ea8c1.webp 1600w" width="800" height="243" alt="Ubuntu User Info" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upgrade to Ubuntu Pro. Skip for now.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_pro_install_hu_f0dd30099558b5e5.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_pro_install_hu_ec68485068bab42.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_pro_install_hu_1066641ddbfba3ec.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_pro_install_hu_1066641ddbfba3ec.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_pro_install_hu_2e6b5a89b9701809.webp 1600w" width="800" height="162" alt="Ubuntu Upgrade to Pro?" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SSH Config. Select Install OpenSSH server.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_ssh_install_hu_d2d8adda282cf3cf.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_ssh_install_hu_340ae1527de89a48.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_ssh_install_hu_da469ac35120b2f1.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_ssh_install_hu_da469ac35120b2f1.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_ssh_install_hu_6e699e2154483ec1.webp 1600w" width="800" height="202" alt="Ubuntu SSH Install" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Third-party drivers. If you have any, install them here.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_3rd_install_hu_a2a85521bb06801a.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_3rd_install_hu_df99b8c82369c55d.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_3rd_install_hu_bf7976c71f9f8d2e.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_3rd_install_hu_bf7976c71f9f8d2e.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_3rd_install_hu_f61d40725d82ff8e.webp 1600w" width="800" height="49" alt="Ubuntu Third-party Drivers" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Featured Server Snaps. Leave all of these blank.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_snaps_install_hu_266a89be45388a11.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_snaps_install_hu_3ba7d2022c995389.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_snaps_install_hu_6609cb22b0698142.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_snaps_install_hu_6609cb22b0698142.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_snaps_install_hu_b341d76546d0a362.webp 1600w" width="800" height="348" alt="Ubuntu Select snaps" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The install takes a few minutes. When it&amp;rsquo;s done, remove the USB stick and reboot.
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_reboot_install_hu_f9b6e5e57179879.avif 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_reboot_install_hu_f5c681e8db9e056e.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_reboot_install_hu_558deef64e3b6aa3.webp" srcset="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_reboot_install_hu_558deef64e3b6aa3.webp 800w, https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-1-hardware-and-installing-linux/ubuntu_reboot_install_hu_f0eb7e573045705.webp 1600w" width="800" height="617" alt="Ubuntu Server Install Reboot" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Login. Confirm you can log in with the username and password you set during the install.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Update the server. Run this to pull any updates. I run it about once a week so the box stays current on security patches.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;Reboot from the command line.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo reboot
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Sharp-eyed readers will notice I&amp;rsquo;m installing this on a VM, not the i5-2500k. That&amp;rsquo;s how I captured the screenshots. I&amp;rsquo;ll install Ubuntu on the physical box and use it going forward.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Congratulations. You&amp;rsquo;ve set up an Ubuntu server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to bring your media server to life? Head to Part 2 to install Jellyfin and get one step closer to streaming your library.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-2-adding-a-storage-drive-and-setting-up-smb-shares/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 2 - Storage and SMB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-3-installing-jellyfin-and-its-dependencies/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 3 - Installing Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://diymediaserver.com/post/turn-an-old-computer-into-a-media-server-part-4-how-to-configure-jellyfin/" target="_blank" rel="noopener"
&gt;Turn an Old Computer into a Media Server PART 4 - Installing Configure Jellyfin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Stop Winging It - Use Obsidian to Master Your Server and Network Notes</title><link>https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/</link><pubDate>Sat, 04 Jan 2025 07:22:27 -0700</pubDate><guid>https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/</guid><description>&lt;img src="https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_main2_hu_7e079124b88b67b4.webp" alt="Featured image of post Stop Winging It - Use Obsidian to Master Your Server and Network Notes" /&gt;&lt;p&gt;Ever feel like the deeper you go into self-hosting, the more it slips out of your head? Proxmox, Docker, Jellyfin, the ARR suite. It piles up fast, and the little details disappear first. That&amp;rsquo;s where Obsidian comes in. Think of it as your personal command center for every note, command, and half-finished guide you&amp;rsquo;ve collected while building your homelab.&lt;/p&gt;
&lt;p&gt;Honestly, I wish I&amp;rsquo;d started using it sooner. For years I worked on my home server without writing down a thing. Now I&amp;rsquo;m stuck retracing my steps, trying to remember how I configured something or what fixed that one weird issue six months ago. It&amp;rsquo;s frustrating. And it taught me the hard way how valuable a well-organized set of notes really is.&lt;/p&gt;
&lt;p&gt;So here&amp;rsquo;s how Obsidian can save you from that same pain and give your homelab documentation a real home.&lt;/p&gt;
&lt;h2 id="why-documenting-your-journey-matters"&gt;Why Documenting Your Journey Matters
&lt;/h2&gt;&lt;p&gt;A home server isn&amp;rsquo;t a one-time project. It&amp;rsquo;s an ongoing adventure. From standing up your first Proxmox node to fine-tuning backups and notification automation, there&amp;rsquo;s a lot to learn and even more to forget. Writing it down helps you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Remember what you did&lt;/strong&gt;: That one-off command that fixed Docker or your NFS share? It&amp;rsquo;s gone the moment you close the terminal unless you write it down.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Save time later&lt;/strong&gt;: When it&amp;rsquo;s time to upgrade hardware or chase a bug six months from now, your own notes will save you hours of Googling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Learn from your mistakes&lt;/strong&gt;: Tracking what worked and what blew up helps you refine your setup and stop repeating the same dumb errors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wish I&amp;rsquo;d started documenting on day one. It would&amp;rsquo;ve saved me weeks of guessing now that I&amp;rsquo;m trying to reconstruct commands, configs, and fixes I figured out years ago. It feels pointless to write things down while you&amp;rsquo;re in the flow. Trust me, your future self will thank you.&lt;/p&gt;
&lt;h2 id="why-i-like-obsidian"&gt;Why I Like Obsidian
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why Obsidian is my go-to for documenting my home server setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Everything in One Place&lt;/strong&gt;: All my notes, commands, and guides live in one spot. No more scattered text files, sticky notes, or screenshots buried in Downloads.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Linked Notes&lt;/strong&gt;: I love how Obsidian lets me link notes together. My &amp;ldquo;Docker Setup&amp;rdquo; note connects straight to my &amp;ldquo;Sonarr Configuration&amp;rdquo; note. It makes it easy to see how the pieces fit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Markdown&lt;/strong&gt;: Obsidian uses plain Markdown. It&amp;rsquo;s lightweight, easy to write, and exports anywhere without extra tools.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Personal Knowledge Base&lt;/strong&gt;: Over time, my Vault has grown into a custom knowledge base tailored to my home server. Every fix, every config, every hard-won lesson.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Offline Access&lt;/strong&gt;: If I knock my internet offline while poking at the firewall (it happens), no problem. Obsidian works entirely offline. Sync the Vault to OneDrive or Google Drive and you can pull it up on any other computer too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Visual Representation of Notes&lt;/strong&gt;: Obsidian&amp;rsquo;s graph view maps how all my notes connect. It&amp;rsquo;s oddly satisfying, and it shows you the shape of your own documentation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features have been a big deal for me. If I&amp;rsquo;d started using Obsidian sooner, I&amp;rsquo;d have saved myself a lot of frustration and made the whole homelab journey smoother.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what my graph view looks like after about 2 months:&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_graph_hu_28e4d3416a89d2b6.avif 800w, https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_graph_hu_47267caa201da249.avif 1600w"&gt;&lt;img src="https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_graph_hu_355642a28d338afb.webp" srcset="https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_graph_hu_355642a28d338afb.webp 800w, https://diymediaserver.com/post/stop-winging-it-use-obsidian-to-master-your-server-and-network-notes/obsidian_graph_hu_21441beae052498e.webp 1600w" width="800" height="755" alt="Obsidian Graph View" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h2 id="getting-started-with-obsidian"&gt;Getting Started with Obsidian
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://obsidian.md/download" target="_blank" rel="noopener"
&gt;Obsidian Download Link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how I use Obsidian to document my home server setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a Vault&lt;/strong&gt;: In Obsidian, a Vault is where all your notes live. I made one specifically for my home server and called it &amp;ldquo;Home Data Center.&amp;rdquo; Pick whatever name makes you smile.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Organize with Categories&lt;/strong&gt;: Set up folders or tags for the things you run. Mine has &amp;ldquo;Proxmox,&amp;rdquo; &amp;ldquo;Docker,&amp;rdquo; &amp;ldquo;Jellyfin,&amp;rdquo; and &amp;ldquo;Usenet.&amp;rdquo; It keeps the mess sorted from day one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document as You Go&lt;/strong&gt;: When you fix a problem, install a new tool, or try something weird, write it down right then. Drop in screenshots and links to whatever guides got you there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Link Related Notes&lt;/strong&gt;: My &amp;ldquo;Radarr Setup&amp;rdquo; note links to my &amp;ldquo;Docker Compose&amp;rdquo; note. Now I can see at a glance how the two depend on each other.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add a Daily Log&lt;/strong&gt;: Obsidian&amp;rsquo;s Daily Notes feature gives me a running journal. What I worked on, what I learned, what broke and how I fixed it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best part about Obsidian is how flexible it is. Whether you write detailed guides or quick bullet points, it bends around how you think.&lt;/p&gt;
&lt;h2 id="practical-examples-of-using-obsidian"&gt;Practical Examples of Using Obsidian
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s how I use Obsidian to keep my home server sane:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Proxmox Setup Notes&lt;/strong&gt;: A step-by-step guide for setting up VMs, with the exact commands, screenshots of the web UI, and links to the official Proxmox docs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting Logs&lt;/strong&gt;: When something breaks, like Sonarr losing its connection to SABnzbd, I document the error and the fix. So I never have to solve the same problem twice.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wish Lists&lt;/strong&gt;: A running list of things I want to add, like a VPN or a reverse proxy, with links to the guides that look promising. (Bonus points if they link back to diymediaserver.com.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Personal Wiki&lt;/strong&gt;: Over time, the Vault has turned into a custom knowledge base for my entire home server setup. As I add services or tweak configs, the notes grow with me.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These examples have saved me hours of repeated frustration. If you&amp;rsquo;re building or running a home server, Obsidian belongs in your stack.&lt;/p&gt;
&lt;h2 id="stick-with-it"&gt;Stick With It
&lt;/h2&gt;&lt;p&gt;If there&amp;rsquo;s one lesson I&amp;rsquo;ve learned the hard way, it&amp;rsquo;s that good documentation is priceless. Trying to rebuild a home server setup from memory is miserable and slow. You don&amp;rsquo;t have to do that.&lt;/p&gt;
&lt;p&gt;Start documenting now with Obsidian. A few minutes of typing today can save you hours, or whole evenings, six months from now. And you&amp;rsquo;ll end up with a record of your progress that&amp;rsquo;s genuinely satisfying to look back on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don&amp;rsquo;t put off documentation. Your future self will thank you. Download Obsidian today and start building your second brain for your media server adventure.&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>Kodi vs Plex vs Jellyfin vs Emby: Which Is Best in 2026?</title><link>https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/</link><pubDate>Fri, 03 Jan 2025 06:21:40 -0700</pubDate><guid>https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/</guid><description>&lt;img src="https://diymediaserver.com/post/kodi-vs-plex-vs-jellyfin-vs-emby-the-ultimate-media-playback-software-showdown/bestsoftware_main2_hu_eb79bf7184978651.webp" alt="Featured image of post Kodi vs Plex vs Jellyfin vs Emby: Which Is Best in 2026?" /&gt;&lt;p&gt;You&amp;rsquo;ve spent hours setting up a home media server. Movies, TV shows, and music are organized and ready to go. Now you have to pick the playback software. Kodi, Plex, Jellyfin, and Emby are the four serious options. Each one solves the same problem in a different way.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve run all four on my own gear, and I landed on a hybrid: Kodi as the front-end player, Jellyfin as the backend server, with the Jellyfin for Kodi add-on bridging the two. Kodi handles the interface and local playback. Jellyfin handles watched-status sync, remote streaming, and transcoding. That combo is what this comparison keeps coming back to, but the right answer for you depends on which trade-offs you care about.&lt;/p&gt;
&lt;h2 id="the-basics-what-do-these-programs-do"&gt;The Basics: What Do These Programs Do?
&lt;/h2&gt;&lt;p&gt;All four programs organize, play, and stream a media library. The differences are in philosophy and architecture.&lt;/p&gt;
&lt;p&gt;Kodi is a standalone media player. It runs on the device you watch on and reads files from local disk or a network share (SMB, NFS, or a mounted drive).&lt;/p&gt;
&lt;p&gt;Plex, Jellyfin, and Emby are server-client systems. One machine runs the server and manages the library, metadata, transcoding, and user accounts. Every TV, phone, and tablet runs a client app that talks to that server.&lt;/p&gt;
&lt;p&gt;Plex and Emby are closed-source and gate their best features behind a paid subscription (Plex Pass and Emby Premiere). Jellyfin is fully open-source and free, forked from Emby in 2018 when Emby went closed. Kodi is open-source under the GPL.&lt;/p&gt;
&lt;h2 id="feature-comparison-at-a-glance"&gt;Feature Comparison: At a Glance
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s how Kodi, Plex, Jellyfin, and Emby stack up:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Kodi&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Plex&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Jellyfin&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Emby&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free (Premium for advanced features)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free (Premium for advanced features)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Offline Playback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live TV/DVR Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (via add-ons)&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming Outside Home&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (with premium)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ease of Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate (more technical)&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wide range of devices supported&lt;/td&gt;
&lt;td&gt;Wide range of devices supported&lt;/td&gt;
&lt;td&gt;Growing list of supported devices&lt;/td&gt;
&lt;td&gt;Wide range of devices supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="my-setup-a-customizable-front-end-on-a-server-backend"&gt;My Setup: A Customizable Front-End on a Server Backend
&lt;/h2&gt;&lt;p&gt;I picked &lt;strong&gt;Kodi as my front-end media player&lt;/strong&gt; because of how much of the interface it lets you change. No recommended shows I didn&amp;rsquo;t ask for, no ads, no upsells. Skins, menu layouts, and add-ons are all editable. If you want full control over what you see when you turn on the TV, Kodi is hard to beat.&lt;/p&gt;
&lt;p&gt;Kodi has limits though. It does not sync watched status across devices, stream to other rooms over the network, or transcode files for clients that can&amp;rsquo;t play them natively. &lt;strong&gt;Jellyfin&lt;/strong&gt; fills those gaps as the backend: run it as a server, install the Jellyfin for Kodi add-on, and you get watched-status sync, one library shared across every device, remote streaming, and on-the-fly hardware transcoding. You keep Kodi&amp;rsquo;s customization and add Jellyfin&amp;rsquo;s server features.&lt;/p&gt;
&lt;p&gt;That pairing is its own decision, though. For most households a plain Jellyfin client is simpler than running Kodi on every TV. If you&amp;rsquo;re deciding between these two, our &lt;a class="link" href="https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/" &gt;Kodi vs Jellyfin comparison&lt;/a&gt; breaks down the trade-off: server vs. client, transcoding, remote access, and when each one wins:&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/featured_hu_d9a5e8ba5cdd0728.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/featured_hu_29b33f9c152b3802.webp" width="100" height="100" alt="Kodi vs Jellyfin (2026): Honest Comparison &amp;#43; Which to Pick" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;&lt;strong&gt;Kodi vs Jellyfin: Which Should You Pick?&lt;/strong&gt;
A head-to-head on server vs. client, transcoding, remote access, and which one fits your household.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/jellyfin-vs-kodi-comparison/"
class="backlink-button"
target="_blank"
rel="noopener noreferrer"&gt;
Read the Comparison
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-pros-and-cons-of-each-platform"&gt;The Pros and Cons of Each Platform
&lt;/h2&gt;&lt;h3 id="kodi-the-customizers-dream"&gt;Kodi: The Customizer&amp;rsquo;s Dream
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deep customization.&lt;/strong&gt; Hundreds of skins, custom menus, and add-ons. The whole UI is yours to rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wide device support.&lt;/strong&gt; Windows, macOS, Linux, Android, Raspberry Pi, Fire TV, Apple TV (via sideload). A Raspberry Pi 4 is plenty for 1080p and most 4K.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline playback.&lt;/strong&gt; Designed for local files. Point it at a folder or a network share and it plays.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free and open-source.&lt;/strong&gt; GPL licensed. Active community.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Steep learning curve.&lt;/strong&gt; Add-on management, skin editing, and library sources take time to learn.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No native remote streaming.&lt;/strong&gt; Plugins and workarounds exist, but nothing matches Plex or Jellyfin out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No backend features.&lt;/strong&gt; No native watched-status sync or cross-device library management. Pair it with Jellyfin, Emby, or Trakt to fill the gap.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Tinkerers who want a fully personalized interface for local playback.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://kodi.tv/download/" target="_blank" rel="noopener"
&gt;Kodi Download Link&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="plex-the-streaming-superstar"&gt;Plex: The Streaming Superstar
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Easy setup.&lt;/strong&gt; The most beginner-friendly of the four. It scans your library, pulls metadata, posters, descriptions, and trailers automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote access.&lt;/strong&gt; Plex.tv brokers connections to your server, so streaming from outside the LAN works without a reverse proxy or VPN on your end.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Friend sharing.&lt;/strong&gt; Share specific libraries with named users and control what they see.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wide device support.&lt;/strong&gt; Smart TVs, Roku, Apple TV, Fire TV, gaming consoles, iOS, Android.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Premium features.&lt;/strong&gt; Plex Pass adds offline downloads, hardware transcoding, live TV, and DVR.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paywall.&lt;/strong&gt; Hardware transcoding, offline downloads, and DVR all require Plex Pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited customization.&lt;/strong&gt; The UI is polished but rigid compared to Kodi.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Closed source.&lt;/strong&gt; Updates, features, and policy changes are on Plex&amp;rsquo;s schedule. Recent direction (more ad-supported streaming, account changes) has pushed some users to Jellyfin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: People who want zero-config remote streaming and don&amp;rsquo;t mind paying for the premium tier.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://www.plex.tv/media-server-downloads/" target="_blank" rel="noopener"
&gt;Plex Download Link&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="jellyfin-the-open-source-hero"&gt;Jellyfin: The Open-Source Hero
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Completely free.&lt;/strong&gt; Remote streaming, live TV, DVR, hardware transcoding, and watched-status sync are all in the base product. No paywall.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy-focused.&lt;/strong&gt; No external account, no telemetry by default. Your library and viewing data stay on your server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-client architecture.&lt;/strong&gt; Central server, plus official apps for Android, iOS, Android TV, Fire TV, Roku, web, and third-party clients including Kodi (via the Jellyfin for Kodi add-on).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transcoding.&lt;/strong&gt; On-the-fly transcoding with VAAPI, QSV, NVENC, and software fallback. Configure in Dashboard &amp;gt; Playback &amp;gt; Transcoding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active community.&lt;/strong&gt; GitHub-driven development, frequent releases, large Matrix/Discord community.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Setup work.&lt;/strong&gt; Running a server, opening ports or setting up a reverse proxy, and configuring transcoding takes more effort than Plex.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client polish varies.&lt;/strong&gt; Roku and tvOS apps have lagged the Android/Fire TV apps in the past. Check current state for your target device before committing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No managed remote access.&lt;/strong&gt; You handle external access yourself (reverse proxy with Caddy or Nginx, Tailscale, or a VPN). Plex hides this with their relay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Open-source-first homelabbers who want the premium feature set without a subscription and don&amp;rsquo;t mind a little networking work.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://jellyfin.org/downloads/" target="_blank" rel="noopener"
&gt;Jellyfin Download Link&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="emby-the-middle-ground"&gt;Emby: The Middle Ground
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Balanced feature set.&lt;/strong&gt; More configurable than Plex, more polished than Jellyfin in places.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote streaming.&lt;/strong&gt; Available via Emby Connect with an Emby Premiere subscription.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live TV and DVR.&lt;/strong&gt; Strong recording, scheduling, and channel management tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wide device support.&lt;/strong&gt; Smart TVs, Roku, Fire TV, Apple TV, mobile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Premium features.&lt;/strong&gt; Emby Premiere covers hardware transcoding, offline downloads, and DVR. Pricing is comparable to Plex Pass.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subscription gates.&lt;/strong&gt; Hardware transcoding, offline sync, and Cloud Sync need Premiere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Closed source since 2018.&lt;/strong&gt; Emby started open, then closed the core. Jellyfin is the fork of the last open version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller community.&lt;/strong&gt; Less third-party add-on development than Kodi or Jellyfin.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Users who want Plex-style polish with a bit more configurability and are willing to pay for premium features.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://emby.media/download.html" target="_blank" rel="noopener"
&gt;Emby Download Link&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="which-one-should-you-choose"&gt;Which One Should You Choose?
&lt;/h2&gt;&lt;p&gt;The right pick depends on what you care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pick Kodi&lt;/strong&gt; for the most customizable interface and local playback on a wide range of devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick Plex&lt;/strong&gt; for zero-config remote streaming, friend sharing, and a polished UI, if Plex Pass is in budget.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick Jellyfin&lt;/strong&gt; for the full feature set with no paywall and no external account.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick Emby&lt;/strong&gt; for a middle ground between Plex&amp;rsquo;s polish and Jellyfin&amp;rsquo;s openness, if you&amp;rsquo;ll pay for Premiere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My pick stays &lt;strong&gt;Kodi plus Jellyfin&lt;/strong&gt;. Jellyfin runs the backend (library, sync, transcoding, remote access). Kodi is the front-end on every TV in the house, fully skinned and laid out the way I want.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts
&lt;/h2&gt;&lt;p&gt;There is no single winner here. Plex wins on ease. Jellyfin wins on price and openness. Emby splits the difference. Kodi wins on interface control. The fastest way to find your answer is to install two of them on a test machine, point them at the same library folder, and watch a few things. Migration between the three server apps is straightforward because they all read the same file structures and use similar metadata (NFO and image sidecar files).&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve picked, the next steps are getting the library structure right (separate folders for Movies and TV, year in folder and filename) and enabling hardware transcoding if you plan to stream to phones or off-LAN.&lt;/p&gt;</description></item><item><title>The Best Devices to Play Your Locally Stored Media with Kodi Plex Jellyfin or Emby</title><link>https://diymediaserver.com/post/best-devices-to-play-your-locally-stored-media/</link><pubDate>Wed, 01 Jan 2025 06:36:03 -0700</pubDate><guid>https://diymediaserver.com/post/best-devices-to-play-your-locally-stored-media/</guid><description>&lt;img src="https://diymediaserver.com/post/best-devices-to-play-your-locally-stored-media/bestdevices_main2_hu_ad77702a751fd857.webp" alt="Featured image of post The Best Devices to Play Your Locally Stored Media with Kodi Plex Jellyfin or Emby" /&gt;&lt;p&gt;So you&amp;rsquo;ve ripped your Blu-rays, downloaded a few movies, and pointed Jellyfin or Kodi at the folder. Now what? You need a playback device on the TV side that won&amp;rsquo;t choke on a 4K HDR remux or fight you over codecs.&lt;/p&gt;
&lt;p&gt;The good news: there&amp;rsquo;s a device for every budget and every level of patience. Here&amp;rsquo;s how the main options actually stack up for a local media library, not for Netflix subscribers.&lt;/p&gt;
&lt;h3 id="1-android-tv-devices-easy-and-capable"&gt;1. Android TV Devices: Easy and Capable
&lt;/h3&gt;&lt;p&gt;If you want something that boots up and works, Android TV boxes like the &lt;strong&gt;NVIDIA Shield TV&lt;/strong&gt; or &lt;strong&gt;Google TV Streamer&lt;/strong&gt; are the obvious picks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why they work&lt;/strong&gt;: They run Android, so the official Kodi and Jellyfin apps install straight from the Play Store. No sideloading, no ADB hoops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: The Shield handles 4K HDR, lossless audio passthrough, and AI upscaling. The Google TV Streamer is solid for 1080p and most 4K, at a fraction of the price.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bonus&lt;/strong&gt;: Both cover Netflix, YouTube, and the rest of the streaming pile, and both support casting from a phone or laptop.&lt;/li&gt;
&lt;/ul&gt;
&lt;script&gt;
(function () {
var ua = navigator.userAgent || "";
if (!/Android|iPhone|iPad|iPod/i.test(ua)) return;
function fix() {
var links = document.querySelectorAll(
'.product-box .affiliate-button[href*="amzn.to"],' +
'.product-box .affiliate-button[href*="amazon."],' +
'.product-box .affiliate-button[href*="link.amazon"]'
);
for (var i = 0; i &lt; links.length; i++) {
links[i].removeAttribute("target");
var rel = (links[i].getAttribute("rel") || "").split(/\s+/)
.filter(function (t) { return t &amp;&amp; t !== "noopener"; }).join(" ");
if (rel) links[i].setAttribute("rel", rel);
else links[i].removeAttribute("rel");
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fix);
} else { fix(); }
})();
&lt;/script&gt;&lt;div class="product-box" data-asin="B07YP9FBMM"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_84a3675c3ec53e96.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/nvidia-shield-tv-pro_hu_541c32e7cf211fab.webp" width="600" height="284" alt="NVIDIA SHIELD Pro" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;NVIDIA SHIELD Pro&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The NVIDIA SHIELD is my go-to device. I&amp;rsquo;ve run Kodi and Jellyfin on it for years and it keeps working.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/4gZhtU2" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B0FN1N8HGT"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/google-tv-streamer_hu_cba5baef67fa49bb.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/google-tv-streamer_hu_5d97c596258d54e3.webp" width="600" height="373" alt="Google TV Streamer 4K" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Google TV Streamer 4K&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A solid runner-up if you can&amp;rsquo;t find a Shield at a sane price. The built-in Ethernet port earns its keep here, because Wi-Fi is where high-bitrate remuxes fall apart.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B0593hJIG" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="2-raspberry-pi-for-people-who-like-to-tinker"&gt;2. Raspberry Pi: For People Who Like to Tinker
&lt;/h3&gt;&lt;p&gt;If you enjoy flashing SD cards and editing config files, the &lt;strong&gt;Raspberry Pi&lt;/strong&gt; is a cheap and capable Kodi box.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why it&amp;rsquo;s cool&lt;/strong&gt;: It&amp;rsquo;s small, cheap, and fully customizable. Flash &lt;strong&gt;LibreELEC&lt;/strong&gt; for a Kodi-only appliance, or run Raspberry Pi OS and put Jellyfin in Docker.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: A Pi 4 handles 1080p comfortably and most 4K H.265 content. Use wired Ethernet if you can. Wi-Fi on the Pi struggles with high-bitrate remuxes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Who it&amp;rsquo;s for&lt;/strong&gt;: DIY folks who want a project rather than a plug-and-play box. You&amp;rsquo;ll pick up real Linux skills on the way.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B07V5JTMV9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/raspberry-pi-4_hu_4d99c73471ffb9c9.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/raspberry-pi-4_hu_8743fb4effa76b2e.webp" width="600" height="459" alt="RaspberryPi 4GB" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;RaspberryPi 4GB&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A palm-sized single-board computer with a quad-core ARM CPU and 4GB RAM that runs full Linux, with Gigabit Ethernet, dual-band Wi-Fi, USB 3.0, and dual micro-HDMI. Ideal for lightweight homelab duties like Pi-hole, Home Assistant, and small Docker stacks, or for learning GPIO-based hardware projects while sipping only a few watts.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3ZXTKg7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="3-amazon-fire-tv-cheap-and-everywhere"&gt;3. Amazon Fire TV: Cheap and Everywhere
&lt;/h3&gt;&lt;p&gt;Amazon&amp;rsquo;s &lt;strong&gt;Fire TV Stick 4K Max&lt;/strong&gt; and &lt;strong&gt;Fire TV Cube&lt;/strong&gt; show up in every &amp;ldquo;what should I buy&amp;rdquo; thread for a reason. They&amp;rsquo;re cheap and they&amp;rsquo;re stocked.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why they work&lt;/strong&gt;: Both support Kodi and Jellyfin. Jellyfin is in the Amazon Appstore in most regions. Kodi needs sideloading via the Downloader app or ADB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: The Stick 4K Max handles 4K HEVC and HDR fine. The Cube is faster and adds Ethernet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Fire OS is Amazon&amp;rsquo;s fork of Android, so the UI is plastered with ads and the remote is built around Alexa. If that bothers you, get a Shield instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="product-box" data-asin="B0BP9SNVH9"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/fire-stick-max_hu_173e095fd7e59c91.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/fire-stick-max_hu_bd12c15141518ecb.webp" width="600" height="494" alt="Amazon Fire TV Stick 4K Max" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Amazon Fire TV Stick 4K Max&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The cheapest way to put Jellyfin on a spare TV. Wi-Fi 6E is the reason to take the Max over the plain 4K stick if you&amp;rsquo;re streaming remuxes over wireless.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://link.amazon/B00vcIMPd" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box" data-asin="B09BZZ3MM7"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/amazonfire-cube_hu_806cb097a84adec2.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/amazonfire-cube_hu_602d65a0e08a720b.webp" width="600" height="600" alt="Amazon Fire TV Cube" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Amazon Fire TV Cube&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most powerful Fire TV, with hands-free Alexa and an IR blaster so it can control your TV, receiver, and other gear by voice. It streams 4K HDR (Dolby Vision/Atmos), adds HDMI-in for set-top boxes, and includes built-in Ethernet plus Wi-Fi 6E for rock-steady playback.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3BQwrgl" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="4-dedicated-htpcs-the-no-compromise-option"&gt;4. Dedicated HTPCs: The No-Compromise Option
&lt;/h3&gt;&lt;p&gt;If you want the best playback you can build, nothing beats a dedicated &lt;strong&gt;Home Theater PC (HTPC)&lt;/strong&gt;. Prebuilt mini PC, repurposed old desktop, or a full custom build. Take your pick.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why they win&lt;/strong&gt;: A real x86 PC running Windows or Linux can chew through anything. Blu-ray remuxes, lossless TrueHD passthrough, madVR, you name it. Pair it with an Intel iGPU or a discrete GPU and hardware decoding handles the rest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customizable&lt;/strong&gt;: Run Windows, a Linux distro, or a Kodi appliance like &lt;a class="link" href="https://libreelec.tv/" target="_blank" rel="noopener"
&gt;LibreELEC&lt;/a&gt;. It&amp;rsquo;s your box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drawbacks&lt;/strong&gt;: It costs the most, takes up the most space, and needs occasional maintenance. Worth it if quality matters more than convenience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-game-consoles-already-in-the-cabinet"&gt;5. Game Consoles: Already in the Cabinet
&lt;/h3&gt;&lt;p&gt;Own an Xbox or PlayStation? You might already have a Jellyfin client.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xbox&lt;/strong&gt;: Both Xbox One and Series consoles have a native Jellyfin app in the Microsoft Store. Kodi runs on Xbox through the UWP build, with some quirks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PlayStation&lt;/strong&gt;: No native Jellyfin app. Use the web client at &lt;code&gt;https://yourserver:8096&lt;/code&gt; in the PS browser, or transcode and stream via Plex if you have a subscription.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why it works&lt;/strong&gt;: The console is already wired to the TV and configured for HDR. Free secondary client.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="6-smartphones-and-tablets-portable-players"&gt;6. Smartphones and Tablets: Portable Players
&lt;/h3&gt;&lt;p&gt;Don&amp;rsquo;t sleep on the device in your pocket. Jellyfin, Kodi, Plex, and Emby all ship Android and iOS apps.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why they work&lt;/strong&gt;: Stream over Wi-Fi at home, or use the Jellyfin app&amp;rsquo;s offline downloads for flights and trains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Features&lt;/strong&gt;: Direct play when codecs match, transcoded streams when they don&amp;rsquo;t. Cast to a Chromecast or AirPlay receiver to throw it on the TV.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best use case&lt;/strong&gt;: Secondary screen in the kitchen, on the couch, or away from home.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="7-smart-tvs-one-less-box"&gt;7. Smart TVs: One Less Box
&lt;/h3&gt;&lt;p&gt;If your TV is recent, check the app store. Both &lt;strong&gt;Samsung Tizen&lt;/strong&gt; and &lt;strong&gt;LG webOS&lt;/strong&gt; have native Jellyfin clients, and most Android TV sets handle Kodi too.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why it&amp;rsquo;s convenient&lt;/strong&gt;: No extra box. No extra remote. One HDMI input free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Older smart TVs are slow, lack codec support for AV1 or HEVC Main 10, and their app stores get abandoned. If your TV is more than five years old, an external box is almost always faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="final-thoughts-pick-the-one-that-fits"&gt;Final Thoughts: Pick the One That Fits
&lt;/h3&gt;&lt;p&gt;There&amp;rsquo;s no single right answer. Match the device to the room, the budget, and how much fiddling you&amp;rsquo;re willing to do.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Living room with a 4K HDR projector? Shield or HTPC.&lt;/li&gt;
&lt;li&gt;Bedroom with a basic 1080p TV? Fire Stick or whatever&amp;rsquo;s already plugged in.&lt;/li&gt;
&lt;li&gt;Tinkering project? Raspberry Pi with LibreELEC.&lt;/li&gt;
&lt;li&gt;Already own a console? Start there before buying anything.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I run a Shield in the main living room and a Pi 4 with LibreELEC in the bedroom. Same Jellyfin server behind both. Two different boxes, same library. That&amp;rsquo;s the whole point.&lt;/p&gt;</description></item><item><title>How to Rip DVDs and Blu Rays for Your Home Media Server</title><link>https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/</link><pubDate>Sat, 28 Dec 2024 07:11:39 -0700</pubDate><guid>https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/</guid><description>&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/rip_media_hu_1d35db194c2490d5.webp" alt="Featured image of post How to Rip DVDs and Blu Rays for Your Home Media Server" /&gt;&lt;p&gt;Rip your DVDs and Blu-Rays and you&amp;rsquo;ve got every movie and show in one place. No more digging through stacks of discs to find a favorite. You browse the whole collection from any device on your media server. That means you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Search Quickly&lt;/strong&gt;: Use metadata like titles, genres, and cast to find movies in seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organize Effectively&lt;/strong&gt;: Group content by genre, director, year, or any custom structure that suits your style.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stream to Multiple Devices&lt;/strong&gt;: With media server tools like Jellyfin or Plex, you can access your library on smart TVs, laptops, tablets, and smartphones anywhere in your home, or even remotely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Physical DVDs and Blu-Rays wear out. Discs get scratched, warped, or lost, and your collection goes with them. Ripping your discs gives you digital backups so you can enjoy the library for years without worrying about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scratched Discs&lt;/strong&gt;: No more frustration when a favorite movie freezes or skips.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage Challenges&lt;/strong&gt;: Free up space and ditch the bulky disc cases and shelves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discontinued Titles&lt;/strong&gt;: Preserve rare or out-of-print movies that might be impossible to replace.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-process-is-simpler-than-you-think"&gt;The Process is Simpler Than You Think
&lt;/h2&gt;&lt;p&gt;If ripping DVDs and Blu-Rays sounds intimidating, you&amp;rsquo;re not alone. A lot of people assume it&amp;rsquo;s overly technical or needs advanced computer skills. With the right tools, it&amp;rsquo;s surprisingly straightforward.&lt;/p&gt;
&lt;p&gt;Today&amp;rsquo;s software is built to be approachable, often only a few clicks to get going. Tools like &lt;strong&gt;MakeMKV&lt;/strong&gt; and &lt;strong&gt;HandBrake&lt;/strong&gt; have intuitive interfaces that walk you through it. You don&amp;rsquo;t need to be a tech expert. If you can install software and follow basic instructions, you&amp;rsquo;re good to go.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll walk you through what you need, from picking the right hardware and software to organizing and storing your ripped files. And if you hit a snag, I&amp;rsquo;ve got tips and troubleshooting to help. This is a learning process. With a bit of patience and practice, you&amp;rsquo;ll feel confident in no time.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you&amp;rsquo;ll see that ripping your DVDs and Blu-Rays is doable and genuinely rewarding. You&amp;rsquo;ll have a digital library you can enjoy whenever and wherever you want. No more wrestling with physical discs. Take it one step at a time and let&amp;rsquo;s get started.&lt;/p&gt;
&lt;h2 id="what-youll-need"&gt;What You&amp;rsquo;ll Need
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hardware Requirements&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;A DVD/Blu-Ray drive (internal or external).
&lt;ul&gt;
&lt;li&gt;OWC Mercury Pro 16X Blu-ray&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A computer with sufficient storage space for the ripped files.
&lt;ul&gt;
&lt;li&gt;DVDs about 10GB of free space&lt;/li&gt;
&lt;li&gt;Blu-Ray 1080p about 40GB of free space&lt;/li&gt;
&lt;li&gt;Blu-Ray 4k UHD about 100GB of free space&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Optional: Large external / internal hard drives, or NAS for additional storage.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="product-box" data-asin="B0CM293XCL"&gt;
&lt;div class="product-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/images/products/seagate24tb_hu_767d50974d3b4cc.avif"&gt;
&lt;img src="https://diymediaserver.com/images/products/seagate24tb_hu_52166fd8490e270a.webp" width="600" height="865" alt="Seagate Barracuda 24TB Internal Hard Drive" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="product-box-content"&gt;
&lt;div class="product-box-description"&gt;
&lt;p&gt;&lt;strong&gt;Seagate Barracuda 24TB Internal Hard Drive&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 3.5-inch SATA desktop-class drive that delivers huge capacity for bulk storage. Perfect for media libraries, game installs, and cold backups without paying NAS/enterprise premiums. Best for single-drive or light-duty DAS use&lt;/p&gt;
&lt;/div&gt;
&lt;div class="product-meta-row"&gt;
&lt;div class="product-price"&gt;
&lt;strong&gt;Amazon Price:&lt;/strong&gt;
&lt;span class="price-loading"&gt;Loading...&lt;/span&gt;
&lt;span class="price-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="product-availability"&gt;
&lt;strong&gt;Availability:&lt;/strong&gt;
&lt;span class="availability-loading"&gt;Checking...&lt;/span&gt;
&lt;span class="availability-value" style="display:none;"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="product-box-links"&gt;
&lt;a href="https://amzn.to/3HCqnL7" class="affiliate-button" target="_blank" rel="noopener nofollow sponsored"&gt;Amazon&lt;/a&gt;
&lt;/div&gt;
&lt;div class="product-affiliate-disclaimer"&gt;
&lt;small&gt;&lt;em&gt;Contains affiliate links. I may earn a commission at no cost to you.&lt;/em&gt;&lt;/small&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a class="link" href="https://diymediaserver.com/post/how-to-install-sonarr-in-docker/" &gt;How to Install Sonarr in Docker&lt;/a&gt;&lt;/p&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Software Options&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a class="link" href="https://www.makemkv.com/download/" target="_blank" rel="noopener"
&gt;MakeMKV&lt;/a&gt;: MakeMKV is a small but powerful tool for ripping Blu-rays into MKV files. It runs on Windows and macOS and does one thing really well, no extra features, straight to the rip. MakeMKV technically offers a free 30-day beta trial, but here&amp;rsquo;s the catch. You can either download the latest version each month, or grab the updated beta key from the forums to keep it active. So you can use the software for free indefinitely. Even though it&amp;rsquo;s labeled &amp;ldquo;beta,&amp;rdquo; MakeMKV has been in beta for years and shows no sign of changing. For now, there&amp;rsquo;s no reason to pay for it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a class="link" href="https://handbrake.fr/downloads.php" target="_blank" rel="noopener"
&gt;HandBrake&lt;/a&gt;: When MakeMKV rips a Blu-Ray, it creates an MKV file that&amp;rsquo;s an exact copy of the movie on the disc. That often lands at 20 to 30GB or more. HandBrake compresses those files down to something easier to manage without noticeably losing quality. This step isn&amp;rsquo;t mandatory, but storing, playing, and streaming files that big is wasteful when smaller, high-quality versions will do the job.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="step-1-rip-your-dvd--blu-ray-with-makemkv"&gt;Step 1: Rip your DVD / Blu-Ray With MakeMKV
&lt;/h2&gt;&lt;p&gt;First things first. You need to rip your Blu-Ray to get a digital copy of the movie. MakeMKV is perfect for this because it focuses on one thing: pulling a full-size 1080p or 4K MKV file off your Blu-Ray disc. Once it&amp;rsquo;s ripped, you&amp;rsquo;ve got an MKV file you can shrink, convert, or modify later. You can also watch it as-is, but compressing it later is a good move to save space.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how to rip your Blu-Ray:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Insert the DVD or Blu-Ray disc into your Blu-Ray drive.&lt;/li&gt;
&lt;li&gt;Open MakeMKV. After a few moments, you&amp;rsquo;ll see a large DVD or Blu-ray drive icon appear on the screen.&lt;/li&gt;
&lt;li&gt;Click the icon to begin scanning the titles on your disc.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv_hu_441420ee70c1381c.avif"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv_hu_9ce07848ddab3eed.webp" width="480" height="388" alt="MakeMKV" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;After MakeMKV scans the disc, you&amp;rsquo;ll see a list of titles on the left. That list includes everything on the disc: main movie, special features, deleted scenes, the lot. If you only want the main movie, look for the largest track. It&amp;rsquo;s usually around 20-30GB. Select the track(s) you want and skip the rest. It might take a little trial and error to find the right one, but the largest track is almost always the main movie.&lt;/p&gt;
&lt;p&gt;On the right side of the window, choose the folder where you want to save the MKV file. Pick a hard drive with plenty of space. The &lt;strong&gt;Info&lt;/strong&gt; section shows an estimated file size, but keep at least 20GB of headroom available. Once everything is set, click the &lt;strong&gt;Make MKV&lt;/strong&gt; button (the one with the green arrow) to start ripping.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv2_hu_3322a9b38d4d8c42.avif"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv2_hu_969050f7d92782e2.webp" width="480" height="500" alt="MakeMKV Titles" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;MakeMKV will take about 20 to 30 minutes to rip your movie, depending on drive speed and the size of the movie. You&amp;rsquo;ll see a green progress bar showing how far along it is. Need to stop the rip? Click the orange stop icon to cancel.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv3_hu_78dcba3cc1278593.avif"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/makemkv3_hu_e73fe40a80f2b5e1.webp" width="480" height="500" alt="MakeMKV Process" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;Once the rip is complete, MakeMKV pops up a confirmation. You can eject the disc and, if you want, drop another one in to start the next rip.&lt;/p&gt;
&lt;p&gt;At this point, if you&amp;rsquo;re eager to watch your movie, open the MKV file with VLC, Plex, Kodi, or any other player that supports MKV. If you&amp;rsquo;re not worried about saving hard drive space, you&amp;rsquo;re done.&lt;/p&gt;
&lt;p&gt;But if you want a cleaner, more organized, space-efficient media library, the next step is to compress the file. That&amp;rsquo;s coming up next.&lt;/p&gt;
&lt;h2 id="step-2-compress-your-rip-to-a-manageable-size-with-handbrake"&gt;Step 2: Compress Your Rip to a Manageable Size with HandBrake
&lt;/h2&gt;&lt;p&gt;To compress your freshly ripped MKV file, open &lt;strong&gt;HandBrake&lt;/strong&gt; and pick &lt;strong&gt;File&lt;/strong&gt; if you&amp;rsquo;re working on a single video. Got multiple rips to convert? Choose &lt;strong&gt;Folder (Batch Scan)&lt;/strong&gt; to scan several files at once. This step only scans and gathers details, so it&amp;rsquo;s safe to point it at an entire folder of rips. You&amp;rsquo;ll decide how to convert each file individually later.&lt;/p&gt;
&lt;p&gt;For this example, I&amp;rsquo;ll keep it simple and &lt;strong&gt;drag and drop&lt;/strong&gt; my new MKV file straight into HandBrake.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake_hu_4dbdd8b50dc1e921.avif 800w, https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake_hu_e62d454f14d067d0.avif 1296w"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake_hu_8b48e26d51b93ec2.webp" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake_hu_8b48e26d51b93ec2.webp 800w, https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake_hu_c070dc797d198d3f.webp 1296w" width="800" height="520" alt="Handbrake" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;p&gt;Next you will need to select the quality.&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake2_hu_cebe0b98e4b4555c.avif 800w, https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake2_hu_20f5f260d146ccf9.avif 926w"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake2_hu_85a89e235ce1facc.webp" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake2_hu_85a89e235ce1facc.webp 800w, https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake2_hu_b08c93198c1382b4.webp 926w" width="800" height="829" alt="Handbrake Quality" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;h3 id="choosing-the-right-quality-settings-in-handbrake"&gt;Choosing the Right Quality Settings in HandBrake
&lt;/h3&gt;&lt;p&gt;Now the tricky part: picking the right quality settings for video compression. HandBrake&amp;rsquo;s presets make it easier to balance video quality and file size. The best preset depends on how much detail you want to keep for each movie. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For a visually loud movie like &lt;em&gt;Transformers: Rise of the Beasts&lt;/em&gt;, you want to preserve every detail of the robot battles and special effects.&lt;/li&gt;
&lt;li&gt;Compressing &lt;em&gt;Monty Python and the Holy Grail&lt;/em&gt; won&amp;rsquo;t lose much. It&amp;rsquo;s a low-budget comedy, and the jokes don&amp;rsquo;t lean on high visual fidelity. They&amp;rsquo;re funny at any resolution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that in mind, here are three compression options for different needs:&lt;/p&gt;
&lt;h4 id="1-ultra-high-quality-uhd-4k"&gt;1. Ultra High Quality, UHD (4K)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Movies with detailed special effects, breathtaking visuals, or films where 4K resolution is crucial to the viewing experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommended Preset:&lt;/strong&gt; &lt;em&gt;Super HQ 2160p60 4K HEVC Surround&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;This preset keeps the full 4K UHD resolution while compressing the file, significantly reducing its size without any noticeable loss in quality. It&amp;rsquo;s perfect for action-packed blockbusters, visually rich films, or nature documentaries where every detail counts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio Tip:&lt;/strong&gt; To keep the original high-quality audio, go to the &lt;strong&gt;Audio&lt;/strong&gt; tab and change the codec from &amp;ldquo;AAC&amp;rdquo; to &amp;ldquo;DTS Passthru,&amp;rdquo; &amp;ldquo;TrueHD Passthru,&amp;rdquo; or &amp;ldquo;AC3 Passthru,&amp;rdquo; depending on the source audio format. This keeps the audio as crisp and immersive as it was on the original disc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2-high-quality-high-resolution-1080p"&gt;2. High Quality, High Resolution (1080p)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Movies with detailed special effects, beautiful visuals, or films you want to keep looking sharp.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommended Preset:&lt;/strong&gt; &lt;em&gt;Super HQ 1080p30 Surround&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;This preset keeps the full 1080p resolution while significantly reducing the file size without sacrificing much quality. It&amp;rsquo;s ideal for visually rich movies where you want to maintain detail without the massive file size of a full rip.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio Tip:&lt;/strong&gt; To preserve the original audio quality, go to the &lt;strong&gt;Audio&lt;/strong&gt; tab and change the codec from &amp;ldquo;AAC&amp;rdquo; to &amp;ldquo;DTS Passthru&amp;rdquo; or &amp;ldquo;AC3 Passthru,&amp;rdquo; depending on the disc&amp;rsquo;s original audio format.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="3-high-quality-lower-resolution-720p"&gt;3. High Quality, Lower Resolution (720p)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Movies where visuals aren&amp;rsquo;t as crucial or older films that don&amp;rsquo;t really benefit from high-definition resolution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommended Preset:&lt;/strong&gt; &lt;em&gt;Super HQ 720p30 Surround&lt;/em&gt; or &lt;em&gt;HQ 720p30 Surround&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;Dropping from 1080p to 720p may sound like a big downgrade, but the difference is often barely noticeable, especially with a high-quality preset that uses minimal compression.&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s a great option for comedies, older movies, or films where you don&amp;rsquo;t need to preserve every tiny detail. A well-compressed 720p file can look better than an overly compressed 1080p version.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="4-low-quality-lower-resolution-720p-or-below"&gt;4. Low Quality, Lower Resolution (720p or Below)
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Movies where saving space is the priority, and video quality isn&amp;rsquo;t a big concern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recommended Preset:&lt;/strong&gt; &lt;em&gt;Very Fast 720p30&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;This setting drastically reduces file size, making it perfect for your &amp;ldquo;guilty pleasure&amp;rdquo; collection or movies you&amp;rsquo;re not worried about keeping in high quality. It&amp;rsquo;s the go-to choice when you need to maximize storage space.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-to-consider"&gt;What to Consider
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual Importance:&lt;/strong&gt; If the movie relies heavily on visuals (like sci-fi or action films), pick a higher-quality preset to preserve detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage Needs:&lt;/strong&gt; For comedies, older films, or &amp;ldquo;background&amp;rdquo; movies, opting for lower resolution or quality presets can save a lot of hard drive space.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Case-by-Case Flexibility:&lt;/strong&gt; The best part? You don&amp;rsquo;t have to use a one-size-fits-all approach. You can dial in the quality for each movie based on how much you care about its visuals versus how much storage space you want to save.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These presets help you find the right balance between quality and storage, so your media library looks great without wasting space.&lt;/p&gt;
&lt;p&gt;For most people, the basic presets work fine and deliver a solid mix of quality and file size. But if you&amp;rsquo;re comfortable with advanced settings and want more control, you can tweak options in the &lt;strong&gt;Video&lt;/strong&gt;, &lt;strong&gt;Audio&lt;/strong&gt;, and &lt;strong&gt;Subtitles&lt;/strong&gt; tabs.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Video Tab:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If the preset&amp;rsquo;s video quality isn&amp;rsquo;t high enough, drop the &lt;strong&gt;RF (Rate Factor)&lt;/strong&gt; to a lower value. The default is usually &lt;strong&gt;18&lt;/strong&gt;. Drop it to &lt;strong&gt;16&lt;/strong&gt; and quality goes up, at the cost of a larger file.&lt;/li&gt;
&lt;li&gt;Change the &lt;strong&gt;Framerate&lt;/strong&gt; setting from &amp;ldquo;30&amp;rdquo; to &lt;strong&gt;&amp;ldquo;Same as Source&amp;rdquo;&lt;/strong&gt; to match the original framerate of your rip for smoother playback.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&amp;rsquo;t hesitate to experiment with these settings to find the right balance of quality and file size for your needs.&lt;/p&gt;
&lt;h3 id="choosing-the-right-container-mp4-vs-mkv"&gt;Choosing the Right Container: MP4 vs. MKV
&lt;/h3&gt;&lt;p&gt;Under the &lt;strong&gt;&amp;ldquo;Container&amp;rdquo;&lt;/strong&gt; setting, pick between &lt;strong&gt;MP4&lt;/strong&gt; and &lt;strong&gt;MKV&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MKV&lt;/strong&gt; is more flexible and feature-rich. It supports multiple audio tracks, subtitles, and slightly higher video quality. It&amp;rsquo;s a great choice for media servers and modern devices that support MKV playback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MP4&lt;/strong&gt; is more universally compatible, especially with mobile devices like iPhones and iPads, as well as older hardware.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose &lt;strong&gt;MKV&lt;/strong&gt; if you want maximum flexibility and quality. Choose &lt;strong&gt;MP4&lt;/strong&gt; if you need broader compatibility across devices.&lt;/p&gt;
&lt;p&gt;To decide, check the device you plan to use for playback. (There&amp;rsquo;s a table at the bottom of this post with the pros and cons of each.)&lt;/p&gt;
&lt;figure class="center"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake3_hu_fc91ab0376577d27.avif"&gt;&lt;img src="https://diymediaserver.com/post/how-to-rip-dvds-and-blu-rays-for-your-home-media-server/handbrake3_hu_a21d341a0e4ca037.webp" width="502" height="435" alt="Handbrake Output" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;If your device supports MKV, choose &lt;strong&gt;MKV&lt;/strong&gt; for the features and superior quality.&lt;/li&gt;
&lt;li&gt;If MKV isn&amp;rsquo;t supported, go with &lt;strong&gt;MP4&lt;/strong&gt; for maximum compatibility across devices.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick the format that best fits your needs.&lt;/p&gt;
&lt;p&gt;When you&amp;rsquo;re ready, click the green &lt;strong&gt;Start Encode&lt;/strong&gt; button to begin the conversion. Working with multiple rips? Click &lt;strong&gt;Add to Queue&lt;/strong&gt; after setting up the current video, then move on to the next title. Once you&amp;rsquo;ve configured presets for all your movies, click the green &lt;strong&gt;Start Queue&lt;/strong&gt; button to process them all at once. Batch processing is a huge time-saver when you&amp;rsquo;ve got several files to convert.&lt;/p&gt;
&lt;p&gt;Once your files are done converting, you&amp;rsquo;ll notice they&amp;rsquo;re much smaller. How much smaller depends on the settings you chose. Play the converted files to check the quality. If everything looks good, you can safely delete the original rips to free up storage space. Your movies are ready to drop into your media library.&lt;/p&gt;
&lt;h3 id="comparison-table-mp4-vs-mkv"&gt;Comparison Table: MP4 vs. MKV
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MP4&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MKV&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Smaller, typically used for compressed files.&lt;/td&gt;
&lt;td&gt;Larger, especially when storing lossless, uncompressed rips.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often compressed, which may result in some loss of quality.&lt;/td&gt;
&lt;td&gt;Supports lossless storage, retaining the full quality of video, audio, and subtitles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audio Tracks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited support for multiple audio tracks.&lt;/td&gt;
&lt;td&gt;Fully supports multiple audio tracks (e.g., different languages, commentary tracks).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subtitle Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited; embedding multiple subtitle tracks can be tricky.&lt;/td&gt;
&lt;td&gt;Excellent support for multiple subtitle tracks, including soft (toggleable) subtitles.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Universally supported across most devices and platforms, including older hardware.&lt;/td&gt;
&lt;td&gt;Widely supported on modern media players, but not always natively compatible with older devices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advanced Features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic; lacks advanced features like chapter markers and high-end audio codecs.&lt;/td&gt;
&lt;td&gt;Supports advanced features like chapters, menus, and high-resolution audio formats (e.g., DTS-HD, Dolby Atmos).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ease of Use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Great for simple playback on most devices without any special configurations.&lt;/td&gt;
&lt;td&gt;More feature-rich but may require specific software (e.g., Jellyfin, VLC, or Plex) for optimal playback.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compression&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ideal for compressed files when storage space is limited.&lt;/td&gt;
&lt;td&gt;Best for lossless files or preserving full-quality rips of DVDs/Blu-Rays.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Future-Proofing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sufficient for standard-definition or compressed high-definition video.&lt;/td&gt;
&lt;td&gt;Better suited for high-resolution formats (e.g., 4K UHD) and high-quality audio codecs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recommended Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;For smaller, portable files or when compatibility with older devices is required.&lt;/td&gt;
&lt;td&gt;For archiving DVDs/Blu-Rays or building a high-quality media library.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description></item><item><title>Building Your Own Legal Media Collection</title><link>https://diymediaserver.com/post/building-your-own-legal-media-collection/</link><pubDate>Wed, 25 Dec 2024 06:51:55 -0700</pubDate><guid>https://diymediaserver.com/post/building-your-own-legal-media-collection/</guid><description>&lt;img src="https://diymediaserver.com/post/building-your-own-legal-media-collection/legal_library_hu_38dc7590d9a536ec.webp" alt="Featured image of post Building Your Own Legal Media Collection" /&gt;&lt;p&gt;A home media server lets you run your own personal Netflix. But you&amp;rsquo;ve got to stay inside the legal fence when you fill it. Sure, you can grab a pirate hat and sail the high seas for content. Before you do, understand what&amp;rsquo;s on the line.&lt;/p&gt;
&lt;h2 id="legal-implications-of-pirating-media"&gt;Legal Implications of Pirating Media
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Civil Lawsuits:&lt;/strong&gt; Copyright holders can sue anyone who illegally distributes their content. These lawsuits usually end with hefty financial penalties.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Criminal Charges:&lt;/strong&gt; In serious cases, especially large-scale piracy or anything done for money, criminal charges can land on top of the civil suit. That means significant fines and even jail time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Statutory Damages:&lt;/strong&gt; Under U.S. copyright law, penalties range from $750 to $30,000 per work. If the court finds the violation was willful, that ceiling jumps to $150,000 per work.&lt;/p&gt;
&lt;h3 id="examples-of-fines-and-penalties"&gt;Examples of Fines and Penalties
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Recording Industry Association of America (RIAA) Lawsuits:&lt;/strong&gt; In the mid-2000s, the RIAA filed thousands of lawsuits against people who downloaded music through peer-to-peer networks. Settlements usually ran from a few thousand to several thousand dollars per person.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Movie Studios and Copyright Groups:&lt;/strong&gt; Movie studios and copyright enforcement groups have done the same thing, going after individuals and piracy websites. Settlement amounts vary, but they typically cover lost revenue and legal fees.&lt;/p&gt;
&lt;h3 id="high-profile-case-examples"&gt;High-Profile Case Examples
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;BMG v. Cox Communications:&lt;/strong&gt; In this landmark case, Cox Communications was found liable for copyright infringement by its users. The result: a $25 million judgment in favor of BMG.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Capitol Records v. Thomas-Rasset:&lt;/strong&gt; Jammie Thomas-Rasset was fined $222,000 for downloading and sharing 24 songs on Kazaa. The fine bounced around through multiple appeals, but the lesson stuck. Piracy can get very expensive, very fast.&lt;/p&gt;
&lt;h2 id="legal-methods-to-add-content-to-your-media-server"&gt;Legal Methods to Add Content to Your Media Server
&lt;/h2&gt;&lt;h3 id="digital-marketplaces"&gt;Digital Marketplaces
&lt;/h3&gt;&lt;p&gt;You can buy or rent movies and shows from platforms like &lt;strong&gt;Amazon Prime Video, Google Play Movies,&lt;/strong&gt; and &lt;strong&gt;iTunes.&lt;/strong&gt; Some allow offline downloads, but they&amp;rsquo;re typically DRM-protected. That means you can&amp;rsquo;t easily move them onto your media server. Always check the terms of service for DRM-free options.&lt;/p&gt;
&lt;h3 id="physical-media-dvds-and-blu-rays"&gt;Physical Media (DVDs and Blu-rays)
&lt;/h3&gt;&lt;p&gt;Buying physical DVDs and Blu-rays is the straightforward path. You can use tools like &lt;strong&gt;MakeMKV&lt;/strong&gt; and &lt;strong&gt;HandBrake&lt;/strong&gt; to rip and compress them for your media server. This is generally legal for personal use, but check your local laws before you start ripping.&lt;/p&gt;
&lt;h3 id="public-domain-and-creative-commons"&gt;Public Domain and Creative Commons
&lt;/h3&gt;&lt;p&gt;Some movies and TV shows are in the public domain. They&amp;rsquo;re free to use without permission. Websites like &lt;strong&gt;Public Domain Movies&lt;/strong&gt; and the &lt;strong&gt;Internet Archive&lt;/strong&gt; host these films. Some creators also release work under &lt;strong&gt;Creative Commons licenses,&lt;/strong&gt; which allow free legal distribution under specific conditions.&lt;/p&gt;
&lt;h3 id="special-offers-and-bundles"&gt;Special Offers and Bundles
&lt;/h3&gt;&lt;p&gt;Watch for deals on digital marketplaces. Services like &lt;strong&gt;Humble Bundle&lt;/strong&gt; sometimes offer digital movies and documentaries at pay-what-you-want prices. It&amp;rsquo;s a cheap, legal way to grow the library.&lt;/p&gt;
&lt;h3 id="free-streaming-services"&gt;Free Streaming Services
&lt;/h3&gt;&lt;p&gt;Platforms like &lt;strong&gt;Pluto TV, Tubi,&lt;/strong&gt; and &lt;strong&gt;Crackle&lt;/strong&gt; offer free, ad-supported streaming. They typically don&amp;rsquo;t allow downloads, but they&amp;rsquo;re legal and free. Some services do offer temporary downloads for offline viewing.&lt;/p&gt;
&lt;h3 id="library-services"&gt;Library Services
&lt;/h3&gt;&lt;p&gt;Platforms like &lt;strong&gt;Hoopla&lt;/strong&gt; and &lt;strong&gt;Kanopy&lt;/strong&gt; partner with local libraries to provide free streaming of movies and TV shows. You&amp;rsquo;ll need a library card from a participating library. You can&amp;rsquo;t download files directly to your server, but these services expand your legal viewing options at zero cost.&lt;/p&gt;
&lt;h3 id="second-hand-used-media"&gt;Second-Hand (Used) Media
&lt;/h3&gt;&lt;p&gt;Buying used DVDs and Blu-rays is a cheap, legal way to grow your collection. Here are some good places to look:&lt;/p&gt;
&lt;h4 id="local-thrift-stores-and-garage-sales"&gt;Local Thrift Stores and Garage Sales
&lt;/h4&gt;&lt;p&gt;These are goldmines for DVDs and Blu-rays at low prices. You&amp;rsquo;ll occasionally find rare or out-of-print titles. Check the discs for scratches before you buy.&lt;/p&gt;
&lt;h4 id="online-marketplaces"&gt;Online Marketplaces
&lt;/h4&gt;&lt;p&gt;Websites like &lt;strong&gt;eBay, Craigslist,&lt;/strong&gt; and &lt;strong&gt;Facebook Marketplace&lt;/strong&gt; are good for used media. You can often find large collections at reasonable prices. Check seller ratings and reviews first.&lt;/p&gt;
&lt;h4 id="used-bookstores-and-media-shops"&gt;Used Bookstores and Media Shops
&lt;/h4&gt;&lt;p&gt;Many used bookstores also sell DVDs and Blu-rays. They usually inspect discs for quality. Specialty shops that focus on used media will often guarantee the discs are in working order.&lt;/p&gt;
&lt;h4 id="library-sales"&gt;Library Sales
&lt;/h4&gt;&lt;p&gt;Libraries sometimes sell off older DVDs and Blu-rays to make room for new inventory. These sales are great for cheap movies, and the money supports your local library.&lt;/p&gt;
&lt;h3 id="tips-for-buying-second-hand-media"&gt;Tips for Buying Second-Hand Media
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inspect Before Buying:&lt;/strong&gt; Check the disc for scratches or defects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for Completeness:&lt;/strong&gt; Make sure the original case, cover art, and inserts are included if they matter to you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be Patient:&lt;/strong&gt; Finding a specific movie can take time. Check regularly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Negotiate:&lt;/strong&gt; Especially at garage sales or on online marketplaces. A little haggling saves real money.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&amp;rsquo;ve bought your second-hand media, rip the content with &lt;strong&gt;MakeMKV&lt;/strong&gt; and compress it with &lt;strong&gt;HandBrake&lt;/strong&gt; if you need to. That gives you a legal, personal collection for your media server.&lt;/p&gt;
&lt;h2 id="best-practices"&gt;Best Practices
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Always Check the License:&lt;/strong&gt; Confirm any digital content is either DRM-free or that you&amp;rsquo;re allowed to convert it for personal use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stay Informed:&lt;/strong&gt; Copyright laws vary by country and they change. Stay up to date on what&amp;rsquo;s legal in your area.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Support Creators:&lt;/strong&gt; Buying content directly from creators or through official channels means they get paid. That&amp;rsquo;s what keeps the next show getting made.&lt;/p&gt;
&lt;p&gt;Follow these rules and you&amp;rsquo;ll build a real, legal library for your media server. The goal isn&amp;rsquo;t a giant pile of files. It&amp;rsquo;s a curated collection you can enjoy, without a copyright notice landing in your mailbox.&lt;/p&gt;</description></item><item><title>Embark on the Ultimate Home Media Journey</title><link>https://diymediaserver.com/post/embark-on-the-ultimate-home-media-journey/</link><pubDate>Fri, 20 Dec 2024 06:10:50 -0700</pubDate><guid>https://diymediaserver.com/post/embark-on-the-ultimate-home-media-journey/</guid><description>&lt;img src="https://diymediaserver.com/post/embark-on-the-ultimate-home-media-journey/diybuild_hu_3c266e97fc8b5fef.webp" alt="Featured image of post Embark on the Ultimate Home Media Journey" /&gt;&lt;p&gt;Tired of scrolling through streaming services only to find your favorite show has vanished? Worried about how these platforms handle your data? If you&amp;rsquo;ve ever wanted a personal entertainment hub where all your media lives in one place, accessible anytime without subscription services watching over your shoulder, you&amp;rsquo;re in the right spot.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re a seasoned tech enthusiast or you&amp;rsquo;re curious about getting started, I&amp;rsquo;ll walk you through it. We&amp;rsquo;ll cover setting up and tuning a home media server that changes how you store, access, and watch your library.&lt;/p&gt;
&lt;h2 id="what-is-a-home-media-server"&gt;What is a Home Media Server?
&lt;/h2&gt;&lt;p&gt;A home media server is a dedicated computer or Network Attached Storage (NAS) device that holds your digital media. Movies, music, photos, the lot. It acts as a central hub on your home network, letting you organize, stream, and share files across devices. Unlike streaming services hosting content on someone else&amp;rsquo;s servers, a home media server gives you full control over your collection. Offline access, no monthly fees, and the freedom to set things up however you like.&lt;/p&gt;
&lt;h3 id="why-set-up-a-home-media-server"&gt;Why Set Up a Home Media Server?
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Centralized Storage:&lt;/strong&gt; Keep your media in one place instead of scattered across phones, laptops, and external drives. Easier to organize, access, and back up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stream Anywhere in Your Home:&lt;/strong&gt; You can stream to any device on your network. Smart TVs, phones, tablets, computers. You&amp;rsquo;re not stuck with the file formats a streaming service supports because you can transcode files to work on any device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customization and Control:&lt;/strong&gt; You decide how the media is organized, displayed, and accessed. User profiles, parental controls, the works. And you&amp;rsquo;re not at the mercy of a streaming catalog that changes every month.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost-Effective:&lt;/strong&gt; After the upfront cost (which varies depending on what you build), running a home media server is cheap. No monthly fees unless you opt into a premium app or service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Offline Access:&lt;/strong&gt; Your content lives locally. The internet drops? Your library is still there.&lt;/p&gt;
&lt;p&gt;Building a home media server can be as simple or as deep as you want. Repurpose an old PC or a Raspberry Pi with external drives, or go all-in on a dedicated NAS with serious media management software. Either way, you&amp;rsquo;ll pick up a lot about networking, Linux, and digital media along the way.&lt;/p&gt;
&lt;h2 id="my-mission-making-home-media-servers-accessible"&gt;My Mission: Making Home Media Servers Accessible
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve spent the last 15 years learning this stuff the hard way, and I want to pass on what I&amp;rsquo;ve found in a way that&amp;rsquo;s easy to follow. From picking hardware to choosing software, I&amp;rsquo;ll share step-by-step guides, real tips, and the practices that have worked for me.&lt;/p&gt;
&lt;p&gt;Are my methods the only way? Probably not. But they work, and the whole point of building your own server is shaping it to fit how you actually use it.&lt;/p&gt;
&lt;h3 id="easy-to-read-easy-to-understand"&gt;Easy to Read, Easy to Understand
&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ll break down the technical stuff into bites you can chew. Plain English, no jargon for jargon&amp;rsquo;s sake. Whether it&amp;rsquo;s networking basics or the guts of media server software, the goal is clear and practical.&lt;/p&gt;
&lt;h3 id="flattening-the-learning-curve"&gt;Flattening the Learning Curve
&lt;/h3&gt;&lt;p&gt;I still remember my first attempt at setting up a home media server. The learning curve almost made me give up. Don&amp;rsquo;t worry. I&amp;rsquo;ve been through the rough patches and want to make your run smoother. Clear steps, common pitfalls called out early, and enough context to give you the confidence to finish.&lt;/p&gt;
&lt;h3 id="lets-get-started"&gt;Let&amp;rsquo;s Get Started
&lt;/h3&gt;&lt;p&gt;In upcoming posts we&amp;rsquo;ll cover building a home media server on &lt;strong&gt;Linux, Proxmox,&lt;/strong&gt; and &lt;strong&gt;Docker&lt;/strong&gt;, then tuning it with apps like &lt;strong&gt;Jellyfin&lt;/strong&gt; for media management and &lt;strong&gt;Kodi&lt;/strong&gt; for playback. We&amp;rsquo;ll also get into the &lt;strong&gt;Arr suite&lt;/strong&gt; for automated downloads, how &lt;strong&gt;MergerFS&lt;/strong&gt; simplifies storage, and what &lt;strong&gt;Usenet&lt;/strong&gt; brings to content acquisition.&lt;/p&gt;
&lt;p&gt;This is about taking control of your media, protecting your privacy, and getting the satisfaction of building something yourself. Whether you want a small setup to stream movies or a full server with every bell and whistle, I&amp;rsquo;ll walk you through it.&lt;/p&gt;
&lt;p&gt;Stay tuned. Let&amp;rsquo;s build something.&lt;/p&gt;</description></item><item><title>About Me</title><link>https://diymediaserver.com/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/about/</guid><description>&lt;p&gt;So you want out of Big Streaming and their constant price increases? Good. You&amp;rsquo;re in the right place.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m Steve, and online you&amp;rsquo;ll find me as &lt;a class="link" href="https://bsky.app/profile/diymediaserver.com" target="_blank" rel="noopener"
&gt;KryptikWurm&lt;/a&gt;. I&amp;rsquo;ve been building, breaking, and rebuilding home media servers since before most people knew what Kodi, Jellyfin, or Plex were. This blog is the cheat sheet I wish I had when I started. No fluff. Real-world homelab advice from someone who has been in the trenches for over a decade.&lt;/p&gt;
&lt;p&gt;It all started in 2010. Cable TV was garbage. Streaming was a mess. I wanted a clean setup where my media worked for me: on my terms, on my hardware, with no monthly bill attached. So I built it. Then rebuilt it. Then dockerized it. Then Proxmox&amp;rsquo;d and LXC&amp;rsquo;d it. Then dockerized it again. And now? It runs like a dream. Mostly.&lt;/p&gt;
&lt;p&gt;This site exists so you don&amp;rsquo;t have to make the same mistakes I did. Or at least not all of them.&lt;/p&gt;
&lt;h2 id="what-this-blog-is-really-about"&gt;What This Blog Is Really About
&lt;/h2&gt;&lt;p&gt;DIYMediaServer is your blueprint for ditching the noise and building a home media setup that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Private:&lt;/strong&gt; your data stays on your hardware&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Powerful:&lt;/strong&gt; automation handles the boring parts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fully under your control:&lt;/strong&gt; no subscriptions, no paywalls, no middlemen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I cover everything from the basics to the edge cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Proxmox, LXC, and virtualized media stacks&lt;/li&gt;
&lt;li&gt;Docker, containers, and self-hosted apps&lt;/li&gt;
&lt;li&gt;Plex, Jellyfin, and the full Arr suite (Sonarr, Radarr, Prowlarr, and friends)&lt;/li&gt;
&lt;li&gt;Usenet, torrents, and end-to-end automation&lt;/li&gt;
&lt;li&gt;Storage strategies with MergerFS, SnapRAID, and ZFS&lt;/li&gt;
&lt;li&gt;Networking, VPNs, and remote access without handing your data to Google&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have ever screamed at Snap for breaking your stack, or sat through a &amp;ldquo;why won&amp;rsquo;t Sonarr start&amp;rdquo; panic, welcome. You&amp;rsquo;ve found your people.&lt;/p&gt;
&lt;h2 id="my-philosophy"&gt;My Philosophy
&lt;/h2&gt;&lt;p&gt;Keep it simple.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t need enterprise gear or a wall of certifications to run a solid setup. You need the right tools, practical guidance, and a system that fits your life rather than one you copied from a Reddit thread and don&amp;rsquo;t fully understand.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not here to sell you on one right way. I&amp;rsquo;m here to help you find your way, and to keep you from throwing a perfectly good server out the window in the process.&lt;/p&gt;
&lt;h2 id="why-i-built-this"&gt;Why I Built This
&lt;/h2&gt;&lt;p&gt;Because I spent years piecing together forum posts, Reddit rants, and half-finished YouTube tutorials. I wanted a place that cut through the noise and taught real solutions instead of dry step-by-step commands.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re setting up your first NAS or tuning a 12-container media stack, the goal is the same: take control, dodge the landmines, and enjoy the build.&lt;/p&gt;
&lt;h2 id="join-the-mission"&gt;Join the Mission
&lt;/h2&gt;&lt;p&gt;Got a smarter way to run Sonarr in Docker? Want to argue about BTRFS vs. ZFS? Pull up a chair. This site is more than me on a soapbox. It&amp;rsquo;s a community of digital tinkerers who run their own setups and are not afraid to break things along the way.&lt;/p&gt;
&lt;p&gt;Find me on &lt;a class="link" href="https://bsky.app/profile/diymediaserver.com" target="_blank" rel="noopener"
&gt;Bluesky&lt;/a&gt;, &lt;a class="link" href="https://x.com/Kryptikwurm" target="_blank" rel="noopener"
&gt;X&lt;/a&gt;, &lt;a class="link" href="https://github.com/kryptikwurm/" target="_blank" rel="noopener"
&gt;GitHub&lt;/a&gt;, or &lt;a class="link" href="https://www.reddit.com/user/KryptikWurm/" target="_blank" rel="noopener"
&gt;Reddit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s build smarter. Let&amp;rsquo;s build together.&lt;/p&gt;</description></item><item><title>Archives</title><link>https://diymediaserver.com/archives/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/archives/</guid><description/></item><item><title>Arr Stack Docker Compose Generator</title><link>https://diymediaserver.com/page/tools/arr-stack-compose-generator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/arr-stack-compose-generator/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/arr-stack-compose-generator/featured_hu_130fc49c47d33330.webp" alt="Featured image of post Arr Stack Docker Compose Generator" /&gt;&lt;p&gt;Tick the services you want, fill in your user IDs and host paths, and copy the three files a working arr stack needs: the &lt;code&gt;.env&lt;/code&gt;, the &lt;code&gt;docker-compose.yml&lt;/code&gt;, and the launch commands. The output matches the stack from the &lt;a class="link" href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/" &gt;full arr stack guide&lt;/a&gt; exactly.&lt;/p&gt;
&lt;div class="hb-tool hb-tool--stacked" data-arr-stack-compose&gt;
&lt;form class="hb-tool-form" autocomplete="off"&gt;
&lt;fieldset class="hb-tool-inputs"&gt;
&lt;legend&gt;Services&lt;/legend&gt;
&lt;div class="hb-tool-checks"&gt;
&lt;label&gt;&lt;input type="checkbox" data-field="svc-prowlarr" checked&gt; Prowlarr&lt;/label&gt;
&lt;label&gt;&lt;input type="checkbox" data-field="svc-sonarr" checked&gt; Sonarr&lt;/label&gt;
&lt;label&gt;&lt;input type="checkbox" data-field="svc-radarr" checked&gt; Radarr&lt;/label&gt;
&lt;label&gt;&lt;input type="checkbox" data-field="svc-bazarr" checked&gt; Bazarr&lt;/label&gt;
&lt;label&gt;&lt;input type="checkbox" data-field="svc-sabnzbd" checked&gt; SABnzbd&lt;/label&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;fieldset class="hb-tool-inputs"&gt;
&lt;legend&gt;Environment&lt;/legend&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-puid-0"&gt;PUID&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;The user the containers run as. Find both with &lt;code&gt;id youruser&lt;/code&gt; on the Docker host.&lt;/p&gt;
&lt;input type="number" id="arr-puid-0" min="0" max="65535"
value="1000" data-field="puid"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-pgid-0"&gt;PGID&lt;/label&gt;
&lt;input type="number" id="arr-pgid-0" min="0" max="65535"
value="1000" data-field="pgid"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-tz-0"&gt;Timezone&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;IANA name, e.g. &lt;code&gt;Europe/London&lt;/code&gt;. Check yours with &lt;code&gt;timedatectl&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="arr-tz-0"
value="America/Denver" data-field="tz"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-config-0"&gt;Config path&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Host folder holding each app's config subfolder.&lt;/p&gt;
&lt;input type="text" id="arr-config-0"
value="/docker" data-field="config-path"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-downloads-0"&gt;Downloads path&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Host folder where SABnzbd drops completed downloads.&lt;/p&gt;
&lt;input type="text" id="arr-downloads-0"
value="/downloads" data-field="downloads-path"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="arr-media-0"&gt;Media path&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Host media root — needs &lt;code&gt;Movies&lt;/code&gt; and &lt;code&gt;Shows&lt;/code&gt; subfolders.&lt;/p&gt;
&lt;input type="text" id="arr-media-0"
value="/media/Storage" data-field="media-path"&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="hb-tool-outputs"&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="env" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" data-output="env"&gt;Select at least one service to generate the .env file.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="compose" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-yaml" data-output="compose"&gt;Select at least one service to generate the Compose file.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;Launch commands&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="commands" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" data-output="commands"&gt;Select at least one service to generate the launch commands.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="decisions-baked-into-this-compose-file"&gt;Decisions baked into this Compose file
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;One shared &lt;code&gt;/downloads&lt;/code&gt; path.&lt;/strong&gt; SABnzbd, Sonarr, and Radarr all see completed downloads at the exact same container path, so imports work without any remote path mapping: the single most common arr-stack misconfiguration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything joins &lt;code&gt;media_network&lt;/code&gt;.&lt;/strong&gt; A user-defined bridge lets containers resolve each other by name: when Prowlarr asks for your Sonarr address, the answer is &lt;code&gt;http://sonarr:8989&lt;/code&gt;. The network is &lt;code&gt;external&lt;/code&gt; so you can tear the stack down and rebuild it without breaking other containers attached to it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;env_file&lt;/code&gt; instead of per-service environment blocks.&lt;/strong&gt; PUID, PGID, UMASK, and TZ live in one place. Change your timezone once, restart, done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Minimal mounts per service.&lt;/strong&gt; Prowlarr only manages indexers, so it gets no media or download mounts. Bazarr writes subtitles next to your media, so it sees the media folders but not &lt;code&gt;/downloads&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Healthchecks on everything.&lt;/strong&gt; &lt;code&gt;docker ps&lt;/code&gt; shows &lt;code&gt;(healthy)&lt;/code&gt; or &lt;code&gt;(unhealthy)&lt;/code&gt; per app, so you can tell a genuinely working stack from one that&amp;rsquo;s merely running.&lt;/p&gt;
&lt;h2 id="before-you-launch"&gt;Before you launch
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Find your PUID/PGID with &lt;code&gt;id youruser&lt;/code&gt;: the containers must own the config, downloads, and media paths.&lt;/li&gt;
&lt;li&gt;Make sure the &lt;code&gt;Movies&lt;/code&gt; and &lt;code&gt;Shows&lt;/code&gt; subfolders exist under your media path.&lt;/li&gt;
&lt;li&gt;Run the launch commands from the folder holding both files (&lt;code&gt;docker compose&lt;/code&gt; reads &lt;code&gt;.env&lt;/code&gt; automatically).&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_c4d4ce52ea706f03.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/featured_hu_759bf8113c1c8e11.webp" width="100" height="100" alt="Arr Stack Docker Compose: Full Usenet Setup Guide on Proxmox" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;The complete walkthrough this generator is built from: LXC setup, app-by-app configuration, and how the pieces talk to each other.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/complete-arr-stack-proxmox-radarr-sonarr/"
class="backlink-button"
target="_self"
&gt;
Read the full guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Intel Quick Sync Compatibility Checker for Jellyfin</title><link>https://diymediaserver.com/page/tools/quicksync-compatibility/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/quicksync-compatibility/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/quicksync-compatibility/featured_hu_b256733ce79712d5.webp" alt="Featured image of post Intel Quick Sync Compatibility Checker for Jellyfin" /&gt;&lt;p&gt;Pick your Intel CPU generation and see which codecs its iGPU can decode and encode in hardware, plus a plain-English verdict on how it holds up as a Jellyfin transcoding box.&lt;/p&gt;
&lt;div class="hb-tool hb-tool--stacked" data-quicksync-checker&gt;
&lt;form class="hb-tool-form" autocomplete="off"&gt;
&lt;fieldset class="hb-tool-inputs"&gt;
&lt;legend&gt;Your CPU&lt;/legend&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="qsv-gen-0"&gt;Intel CPU generation&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Find yours with &lt;code&gt;lscpu | grep "Model name"&lt;/code&gt; — the first digit(s) of the model number are the generation (i5-&lt;strong&gt;8&lt;/strong&gt;500 = 8th gen).&lt;/p&gt;
&lt;select id="qsv-gen-0" data-field="gen"&gt;
&lt;option value="" selected&gt;— Select your CPU generation —&lt;/option&gt;
&lt;option value="none"&gt;2010 or older, no iGPU, or F-series CPU&lt;/option&gt;
&lt;option value="sandy"&gt;2nd gen Core (Sandy Bridge, 2011) — e.g. i5-2400&lt;/option&gt;
&lt;option value="ivy"&gt;3rd gen Core (Ivy Bridge, 2012) — e.g. i5-3470&lt;/option&gt;
&lt;option value="haswell"&gt;4th gen Core (Haswell, 2013) — e.g. i5-4570&lt;/option&gt;
&lt;option value="broadwell"&gt;5th gen Core (Broadwell, 2014) — e.g. i5-5250U&lt;/option&gt;
&lt;option value="skylake"&gt;6th gen Core (Skylake, 2015) — e.g. i5-6500&lt;/option&gt;
&lt;option value="kaby"&gt;7th gen Core (Kaby Lake, 2017) — e.g. i5-7500&lt;/option&gt;
&lt;option value="gemini"&gt;Gemini Lake Celeron/Pentium (2017) — e.g. J4105, J5005&lt;/option&gt;
&lt;option value="coffee"&gt;8th/9th gen Core (Coffee Lake, 2017–18) — e.g. i5-8500, i5-9500&lt;/option&gt;
&lt;option value="comet"&gt;10th gen Core (Comet Lake, 2020) — e.g. i5-10500&lt;/option&gt;
&lt;option value="gen11"&gt;11th gen Core (Rocket/Tiger Lake, 2020–21) — e.g. i5-11500&lt;/option&gt;
&lt;option value="alder"&gt;12th gen Core (Alder Lake, 2021) — e.g. i5-12500&lt;/option&gt;
&lt;option value="n100"&gt;Intel N-series (Alder Lake-N, 2023) — e.g. N100, N305&lt;/option&gt;
&lt;option value="raptor"&gt;13th/14th gen Core (Raptor Lake, 2022–23) — e.g. i5-13500&lt;/option&gt;
&lt;option value="ultra"&gt;Core Ultra (Meteor/Arrow/Lunar Lake, 2023+)&lt;/option&gt;
&lt;/select&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="hb-tool-outputs"&gt;
&lt;p class="hb-tool-note" aria-live="polite" data-output="verdict"&gt;Select your CPU generation to see what it can transcode.&lt;/p&gt;
&lt;div class="hb-tool-output-block hb-tool-hidden" data-qsv-table-wrap&gt;
&lt;table class="hb-tool-table"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Codec&lt;/th&gt;&lt;th&gt;Typically found in&lt;/th&gt;&lt;th&gt;Decode (input)&lt;/th&gt;&lt;th&gt;Encode (output)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody data-qsv-table&gt;&lt;/tbody&gt;
&lt;/table&gt;
&lt;p class="hb-tool-help" data-qsv-note style="margin-top: 0.6rem;"&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="how-to-read-the-table"&gt;How to read the table
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Decode&lt;/strong&gt; is the input side: can the iGPU read this codec without waking the CPU? &lt;strong&gt;Encode&lt;/strong&gt; is the output side: what Jellyfin re-compresses the stream into for the client (almost always H.264, which every generation with Quick Sync can produce).&lt;/p&gt;
&lt;p&gt;The row that matters most is &lt;strong&gt;HEVC 10-bit (Main10)&lt;/strong&gt;: nearly all 4K HDR content uses it. Hardware decode of HEVC 10-bit is also what enables Jellyfin&amp;rsquo;s hardware &lt;strong&gt;tone mapping&lt;/strong&gt;, which converts HDR to SDR so the picture doesn&amp;rsquo;t look washed out on non-HDR screens. That support arrived with 7th gen (Kaby Lake) in 2017, which is why used 8th/9th-gen office PCs and N100 mini PCs are such popular media-server picks.&lt;/p&gt;
&lt;h2 id="checking-your-hardware-from-the-shell"&gt;Checking your hardware from the shell
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# CPU model (the first digits of i5-8500 → 8th gen)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lscpu &lt;span class="p"&gt;|&lt;/span&gt; grep &lt;span class="s2"&gt;&amp;#34;Model name&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Confirm the iGPU render node exists&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ls -l /dev/dri
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Full codec support report (Debian: apt install vainfo)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vainfo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;vainfo&lt;/code&gt; is the ground truth: each &lt;code&gt;VAProfile&lt;/code&gt; line it prints is a codec/mode your driver actually exposes. If &lt;code&gt;vainfo&lt;/code&gt; shows &lt;code&gt;VAProfileHEVCMain10 : VAEntrypointVLD&lt;/code&gt;, you have HEVC 10-bit decode.&lt;/p&gt;
&lt;h2 id="caveats-worth-knowing"&gt;Caveats worth knowing
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;F-series CPUs (e.g. i5-9400F) have no iGPU&lt;/strong&gt;: no Quick Sync at all, regardless of generation.&lt;/li&gt;
&lt;li&gt;Codec support listed here is the hardware ceiling; you still need the right driver (&lt;code&gt;intel-media-va-driver-non-free&lt;/code&gt; on Debian) and, for tone mapping, the OpenCL runtime (&lt;code&gt;intel-opencl-icd&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Some low-end Celerons/Pentiums within a generation ship cut-down iGPUs. When in doubt, run &lt;code&gt;vainfo&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/featured_hu_bcbeda2d4c823bcf.avif"&gt;&lt;img src="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/featured_hu_ed313df434e7c8bd.webp" width="100" height="100" alt="Jellyfin with Intel QuickSync in an Unprivileged LXC" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;How to pass the iGPU into an unprivileged Proxmox LXC and switch Jellyfin to hardware transcoding.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/"
class="backlink-button"
target="_self"
&gt;
Jellyfin &amp;#43; QuickSync in LXC
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/featured_hu_1e26fb48e9269c4d.avif"&gt;&lt;img src="https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/featured_hu_774e5696865f109a.webp" width="100" height="100" alt="GPU Passthrough Proxmox QuickSync Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;The complete Proxmox iGPU passthrough walkthrough, including driver setup and verifying transcodes actually hit the GPU.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/gpu-passthrough-proxmox-quicksync-guide/"
class="backlink-button"
target="_self"
&gt;
Full GPU passthrough guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>LXC UID/GID Mapping Tool</title><link>https://diymediaserver.com/page/tools/proxmox-lxc-idmap/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/proxmox-lxc-idmap/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/proxmox-lxc-idmap/featured_hu_3c5c9527cb256df1.webp" alt="Featured image of post LXC UID/GID Mapping Tool" /&gt;&lt;p&gt;Enter the UIDs and GIDs you want to pass through from the host into your LXC container. The tool generates the &lt;code&gt;lxc.idmap&lt;/code&gt; block for your container config, the &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; entries needed on the Proxmox host.&lt;/p&gt;
&lt;div class="lxc-idmap-tool" data-lxc-idmap-tool="0"&gt;
&lt;form class="lxc-idmap-form" autocomplete="off"&gt;
&lt;fieldset class="lxc-idmap-inputs"&gt;
&lt;legend&gt;Container settings&lt;/legend&gt;
&lt;div class="lxc-idmap-field"&gt;
&lt;label for="lxc-idmap-ctid-0"&gt;Container ID (CTID) — optional&lt;/label&gt;
&lt;p class="lxc-idmap-help"&gt;The numeric ID of your LXC container, e.g. 101. Used to name the config file in the output.&lt;/p&gt;
&lt;input type="number" id="lxc-idmap-ctid-0"
min="100" max="999999999" placeholder="e.g. 101"
data-lxc-idmap="ctid"&gt;
&lt;/div&gt;
&lt;div class="lxc-idmap-field"&gt;
&lt;label for="lxc-idmap-uids-0"&gt;User IDs (UIDs) to share&lt;/label&gt;
&lt;p class="lxc-idmap-help"&gt;Comma-separated user IDs you want to share with the container. Find with &lt;code&gt;id &amp;lt;username&amp;gt;&lt;/code&gt; on the host.&lt;/p&gt;
&lt;input type="text" id="lxc-idmap-uids-0"
placeholder="e.g. 1000, 1001"
data-lxc-idmap="uids"&gt;
&lt;/div&gt;
&lt;div class="lxc-idmap-field"&gt;
&lt;label for="lxc-idmap-gids-0"&gt;Group IDs (GIDs) to share&lt;/label&gt;
&lt;p class="lxc-idmap-help"&gt;Often the same numbers as the UIDs above. Find with &lt;code&gt;id &amp;lt;username&amp;gt;&lt;/code&gt; on the host.&lt;/p&gt;
&lt;input type="text" id="lxc-idmap-gids-0"
placeholder="e.g. 1000"
data-lxc-idmap="gids"&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;fieldset class="lxc-idmap-toggles"&gt;
&lt;div class="lxc-idmap-toggle-group"&gt;
&lt;span class="lxc-idmap-toggle-label"&gt;Container mode&lt;/span&gt;
&lt;div class="lxc-idmap-toggle" role="radiogroup"&gt;
&lt;label&gt;
&lt;input type="radio" name="lxc-idmap-mode-0" value="unprivileged"
data-lxc-idmap="mode" checked&gt;
Unprivileged
&lt;/label&gt;
&lt;label&gt;
&lt;input type="radio" name="lxc-idmap-mode-0" value="privileged"
data-lxc-idmap="mode"&gt;
Privileged
&lt;/label&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="lxc-idmap-outputs"&gt;
&lt;p class="lxc-idmap-privnote lxc-idmap-hidden" data-lxc-idmap-privnote&gt;
Privileged containers share host UIDs/GIDs directly — no &lt;code&gt;lxc.idmap&lt;/code&gt; or subuid changes are needed. Only a host &lt;code&gt;chown&lt;/code&gt; is required.
&lt;/p&gt;
&lt;div class="lxc-idmap-output-block" data-lxc-idmap-output="idmap"&gt;
&lt;div class="lxc-idmap-output-header"&gt;
&lt;span class="lxc-idmap-output-title"&gt;&lt;code data-lxc-idmap-conftitle&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="lxc-idmap-copy" data-target="lxc-idmap-idmap-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="lxc-idmap-idmap-0"&gt;Enter UID(s) and/or GID(s) to generate output.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="lxc-idmap-output-block" data-lxc-idmap-output="subid"&gt;
&lt;div class="lxc-idmap-output-header"&gt;
&lt;span class="lxc-idmap-output-title"&gt;&lt;code&gt;/etc/subuid&lt;/code&gt; + &lt;code&gt;/etc/subgid&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="lxc-idmap-copy" data-target="lxc-idmap-subid-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="lxc-idmap-subid-0"&gt;Enter UID(s) and/or GID(s) to generate output.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="finding-ids-on-the-proxmox-host"&gt;Finding IDs on the Proxmox Host
&lt;/h2&gt;&lt;p&gt;Run these commands in a Proxmox host shell to find the UID and GID you need:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Your current user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# A specific service user (e.g. jellyfin, www-data)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Look up a user by name in the full user database&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getent passwd jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output looks like &lt;code&gt;uid=1000(jellyfin) gid=1000(jellyfin) groups=...&lt;/code&gt;. The number after &lt;code&gt;uid=&lt;/code&gt; is the UID; the number after &lt;code&gt;gid=&lt;/code&gt; is the primary GID. Enter those numbers into the tool above.&lt;/p&gt;
&lt;h2 id="finding-ids-inside-the-lxc-container"&gt;Finding IDs inside the LXC Container
&lt;/h2&gt;&lt;p&gt;Run these commands from the Proxmox host to query IDs inside a running container (replace &lt;code&gt;101&lt;/code&gt; with your CTID):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Look up a user inside the container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- id jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check what uid_map the container is using&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="m"&gt;101&lt;/span&gt; -- cat /proc/self/uid_map
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or enter the container shell directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct enter &lt;span class="m"&gt;101&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; output inside the container shows the &lt;strong&gt;LXC-side&lt;/strong&gt; IDs. These are what go into the &lt;code&gt;lxc.idmap&lt;/code&gt; lines. The Proxmox host sees the mapped host IDs instead.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_db44a65f7d14b52f.avif"&gt;&lt;img src="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_8abc5b45895fa4b2.webp" width="100" height="100" alt="Mastering UID/GID Mapping in Proxmox LXC Containers" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;For the conceptual background on how UID/GID mapping works, the default offset layout, and common bind-mount troubleshooting.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/"
class="backlink-button"
target="_self"
&gt;
Read the full guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Media Library Storage Calculator</title><link>https://diymediaserver.com/page/tools/media-storage-calculator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/media-storage-calculator/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/media-storage-calculator/featured_hu_36813dde1156c62a.webp" alt="Featured image of post Media Library Storage Calculator" /&gt;&lt;p&gt;Enter how many movies and TV episodes you have (or plan to collect) and the quality you keep them at. The calculator estimates total storage and adds growth headroom so you buy drives once, not twice.&lt;/p&gt;
&lt;div class="hb-tool" data-media-storage-calculator&gt;
&lt;form class="hb-tool-form" autocomplete="off"&gt;
&lt;fieldset class="hb-tool-inputs"&gt;
&lt;legend&gt;Your library&lt;/legend&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="storage-movies-0"&gt;Number of movies&lt;/label&gt;
&lt;input type="number" id="storage-movies-0"
min="0" max="1000000" placeholder="e.g. 500"
data-field="movies"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="storage-movie-quality-0"&gt;Movie quality&lt;/label&gt;
&lt;select id="storage-movie-quality-0" data-field="movie-quality"&gt;
&lt;option value="sd"&gt;SD / 720p (~2 GB each)&lt;/option&gt;
&lt;option value="web1080" selected&gt;1080p web quality (~5 GB each)&lt;/option&gt;
&lt;option value="remux1080"&gt;1080p remux (~25 GB each)&lt;/option&gt;
&lt;option value="web4k"&gt;4K web quality (~15 GB each)&lt;/option&gt;
&lt;option value="remux4k"&gt;4K remux (~60 GB each)&lt;/option&gt;
&lt;/select&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="storage-episodes-0"&gt;Number of TV episodes&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Total episodes, not shows — a typical full series runs 60–150 episodes.&lt;/p&gt;
&lt;input type="number" id="storage-episodes-0"
min="0" max="10000000" placeholder="e.g. 2000"
data-field="episodes"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="storage-episode-quality-0"&gt;TV episode quality&lt;/label&gt;
&lt;select id="storage-episode-quality-0" data-field="episode-quality"&gt;
&lt;option value="sd"&gt;SD / 720p (~0.7 GB each)&lt;/option&gt;
&lt;option value="web1080" selected&gt;1080p web quality (~1.5 GB each)&lt;/option&gt;
&lt;option value="remux1080"&gt;1080p remux (~7 GB each)&lt;/option&gt;
&lt;option value="web4k"&gt;4K web quality (~4 GB each)&lt;/option&gt;
&lt;option value="remux4k"&gt;4K remux (~15 GB each)&lt;/option&gt;
&lt;/select&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="storage-headroom-0"&gt;Growth headroom&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Libraries only grow. MergerFS also wants free space per drive (&lt;code&gt;minfreespace&lt;/code&gt;).&lt;/p&gt;
&lt;select id="storage-headroom-0" data-field="headroom"&gt;
&lt;option value="0"&gt;None — just what I have today&lt;/option&gt;
&lt;option value="20" selected&gt;20% — a year or so of growth&lt;/option&gt;
&lt;option value="50"&gt;50% — room to hoard&lt;/option&gt;
&lt;option value="100"&gt;100% — plan to double&lt;/option&gt;
&lt;/select&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="hb-tool-outputs"&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;Storage estimate&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="estimate" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code data-output="estimate"&gt;Enter your movie and episode counts to estimate storage.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="where-the-size-estimates-come-from"&gt;Where the size estimates come from
&lt;/h2&gt;&lt;p&gt;The per-file averages are deliberately round planning numbers, based on typical release sizes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quality tier&lt;/th&gt;
&lt;th&gt;Movie&lt;/th&gt;
&lt;th&gt;TV episode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SD / 720p&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;td&gt;~0.7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p web quality&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;~1.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p remux&lt;/td&gt;
&lt;td&gt;~25 GB&lt;/td&gt;
&lt;td&gt;~7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K web quality&lt;/td&gt;
&lt;td&gt;~15 GB&lt;/td&gt;
&lt;td&gt;~4 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K remux&lt;/td&gt;
&lt;td&gt;~60 GB&lt;/td&gt;
&lt;td&gt;~15 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;ldquo;Web quality&amp;rdquo; means the compressed encodes most people collect. A &lt;strong&gt;remux&lt;/strong&gt; is the untouched disc stream in a new container: no quality loss, and five to ten times the size. Mixed libraries land between tiers: if you keep remuxes of favourites and web encodes of everything else, run the calculator twice and add the results.&lt;/p&gt;
&lt;h2 id="two-gotchas-when-translating-this-into-drives"&gt;Two gotchas when translating this into drives
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Decimal TB vs. TiB.&lt;/strong&gt; A &amp;ldquo;12 TB&amp;rdquo; drive holds 12 trillion bytes, which your OS reports as roughly 10.9 TiB. The calculator works in decimal TB, same as drive labels, but expect the formatted number on the system to read about 9% lower.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parity is on top.&lt;/strong&gt; If you protect the array with SnapRAID, parity drives don&amp;rsquo;t add usable space. Size them with the &lt;a class="link" href="https://diymediaserver.com/page/tools/snapraid-calculator/" &gt;SnapRAID Parity &amp;amp; MergerFS Pool Calculator&lt;/a&gt; once you know your data-drive layout.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/media-server-storage-2025/featured_hu_5c136c79eecec39.avif"&gt;&lt;img src="https://diymediaserver.com/post/media-server-storage-2025/featured_hu_1e8ef85017d7c76.webp" width="100" height="100" alt="Media Server Storage Guide" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Which drives to actually buy: NAS vs. shucked externals, what to look for on the used market, and how to grow a pool over time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/media-server-storage-2025/"
class="backlink-button"
target="_self"
&gt;
Storage hardware guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>My Hardware</title><link>https://diymediaserver.com/page/my-hardware/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/my-hardware/</guid><description>&lt;p&gt;A running inventory of what I actually run in my lab, no wishlist items, no gear I quietly retired two years ago. This is the fleet as it stands today.&lt;/p&gt;
&lt;h2 id="proxmox-cluster"&gt;Proxmox Cluster
&lt;/h2&gt;&lt;p&gt;Three consumer-grade boxes form the cluster. All three run 12th/13th-gen Intel i5 chips, which means every node has a QuickSync-capable iGPU. That&amp;rsquo;s what powers &lt;a class="link" href="https://diymediaserver.com/post/jellyfin_intel_quicksync_unprivileged_lxc/" &gt;Jellyfin hardware transcoding in an unprivileged LXC&lt;/a&gt;. None of them can do ECC. These are all consumer chipsets and consumer boards.&lt;/p&gt;
&lt;h3 id="pve-01"&gt;pve-01
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Motherboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ASRock Z690 Pro RS&lt;/td&gt;
&lt;td&gt;ATX 4U Rack Mounted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intel Core i5-12600K&lt;/td&gt;
&lt;td&gt;12th gen, QuickSync iGPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64GB DDR4-2400&lt;/td&gt;
&lt;td&gt;4× 16GB Crucial Ballistix Sport LT, all four slots full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot Drive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WD_BLACK SN770 2TB NVMe&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6× 1TB SATA SSD&lt;/td&gt;
&lt;td&gt;ZFS raidz1 pool (5.4TB) holding the backup server&amp;rsquo;s datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Micron 1TB NVMe&lt;/td&gt;
&lt;td&gt;Installed, unpartitioned, nothing planned for it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HBA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8-port SAS/SATA HBA&lt;/td&gt;
&lt;td&gt;Passed through to the TrueNAS VM (&lt;a class="link" href="https://diymediaserver.com/post/hardware-why-you-need-an-hba/" &gt;why you need an HBA&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This node carries the network-critical guests: the OPNsense router VM, the primary Proxmox Backup Server VM, and the TrueNAS VM below. With all four DIMM slots occupied by a matched 16GB kit, the only way to add memory here is to replace the whole set.&lt;/p&gt;
&lt;h3 id="pve-02"&gt;pve-02
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;System&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ace Vision VS-DM660I&lt;/td&gt;
&lt;td&gt;Small-form-factor desktop, sits on top of the rack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Motherboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ASRock B660M-STX/TPM&lt;/td&gt;
&lt;td&gt;Mini-STX: 2 SATA ports, no PCIe slots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intel Core i5-13500&lt;/td&gt;
&lt;td&gt;13th gen: 14 cores (6P+8E) / 20 threads, the widest CPU in the fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64GB DDR4-3200&lt;/td&gt;
&lt;td&gt;2× 32GB Kingston &lt;strong&gt;SODIMM&lt;/strong&gt;, both slots full, and 64GB is the board&amp;rsquo;s hard maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot Drive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Samsung 980 1TB NVMe&lt;/td&gt;
&lt;td&gt;Also holds all guest disks; runs at 55°C, the hottest disk in the lab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2× 1TB SATA SSD&lt;/td&gt;
&lt;td&gt;One is passed whole to the *arr VM as download scratch; the other sits idle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The most expansion-constrained box in the fleet, and the only one that&amp;rsquo;s out of road in &lt;em&gt;every&lt;/em&gt; direction. Mini-STX means no PCIe at all, both SATA ports are already used, and the single free M.2 slot is its only growth path for storage. Memory is worse: the board tops out at 64GB across two slots and both are full, so there is no upgrade short of replacing the machine. Mini-STX also takes laptop SODIMMs rather than the desktop UDIMMs the other two nodes use, which means memory can&amp;rsquo;t be shuffled between this box and the rest of the fleet in either direction.&lt;/p&gt;
&lt;p&gt;Despite all that it&amp;rsquo;s the least stressed node in the lab. It carries the biggest paper overcommit, the guests on it are allocated more RAM than the box physically has, and still sits at roughly a third of memory used with essentially no swap in play. Nothing on it is pinned, so the VMs balloon down when they&amp;rsquo;re idle, and unlike the other two nodes it runs no ZFS pool, so there&amp;rsquo;s no ARC cache competing with the guests for what&amp;rsquo;s left. It earns its keep running the app VMs and most of the LXCs.&lt;/p&gt;
&lt;h3 id="pve-03"&gt;pve-03
&lt;/h3&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Motherboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ASUS PRIME B760-PLUS D4&lt;/td&gt;
&lt;td&gt;ATX 4U Rack Mounted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intel Core i5-12500&lt;/td&gt;
&lt;td&gt;12th gen, QuickSync iGPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64GB DDR4-3200&lt;/td&gt;
&lt;td&gt;4× 16GB Samsung, replaced July 2026, see below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot Drive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WD SN810 1TB NVMe&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Micron 1TB + Samsung 980 1TB NVMe&lt;/td&gt;
&lt;td&gt;ZFS mirror (928GB), second backup-server datastore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HBA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LSI SAS3008 (9300-8i class)&lt;/td&gt;
&lt;td&gt;Passed through to the storage VM below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This node also runs the second Proxmox Backup Server, which pulls the primary&amp;rsquo;s datastore nightly, so the backups no longer live on one node.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The memory here was replaced in July 2026.&lt;/strong&gt; The previous set was a mismatched pair of 8GB G.Skill and 16GB Kingston sticks, 48GB total, downclocked to 2133 MT/s. It was diagnosed silently flipping bits, corrupting two chunks in the backup mirror three days apart. Two things made that failure nasty: the RAM is non-ECC, so there is no error reporting at all and a clean system journal proves nothing, and ZFS can&amp;rsquo;t save you from it either. The bytes were already wrong when ZFS checksummed them, so the pool reported zero checksum errors while faithfully storing the corruption. A matched 64GB kit went in and the nightly backup verify job is now the standing detector.&lt;/p&gt;
&lt;h2 id="bulk-storage-powderkeg"&gt;Bulk Storage: PowderKeg
&lt;/h2&gt;&lt;p&gt;The media pool: six drives, individually formatted XFS and merged into one ~95TB volume with &lt;a class="link" href="https://diymediaserver.com/post/master-the-basics-mergerfs-the-best-way-to-combine-drives-for-your-home-media-server/" &gt;mergerfs&lt;/a&gt;, served to the LAN over NFS and SMB. It runs as a VM with the HBA passed straight through, so the drives belong to the guest rather than the host.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Drive&lt;/th&gt;
&lt;th&gt;Capacity&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2× Seagate Barracuda (ST24000DM001)&lt;/td&gt;
&lt;td&gt;24TB each&lt;/td&gt;
&lt;td&gt;The newest pair, replaced the smaller drives from the original build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2× WD White Label (WD140EDGZ)&lt;/td&gt;
&lt;td&gt;14TB each&lt;/td&gt;
&lt;td&gt;Shucked from WD Elements externals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2× Seagate Exos X16 (ST14000NM001G)&lt;/td&gt;
&lt;td&gt;14TB each&lt;/td&gt;
&lt;td&gt;Enterprise drives, quiet enough for the rack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No RAID, no parity: mergerfs keeps whole files per disk, so a dead drive loses only that drive&amp;rsquo;s files, and the media is all re-acquirable. The irreplaceable data lives on the fast pool instead.&lt;/p&gt;
&lt;h2 id="fast-storage-treasurechest"&gt;Fast Storage: TreasureChest
&lt;/h2&gt;&lt;p&gt;A TrueNAS SCALE VM with its own HBA passthrough: &lt;strong&gt;8× Samsung 860 EVO 500GB&lt;/strong&gt; SSDs in a single raidz2 vdev: 3.6TB raw, two drives&amp;rsquo; worth of parity. This is where the irreplaceable data lives: the family photo library, exported over NFS, snapshotted daily and synced offsite weekly.&lt;/p&gt;
&lt;h2 id="networking"&gt;Networking
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Router&lt;/td&gt;
&lt;td&gt;OPNsense (virtualized)&lt;/td&gt;
&lt;td&gt;Runs as a VM on pve-01 no dedicated router box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch&lt;/td&gt;
&lt;td&gt;16-port 1GbE unmanaged&lt;/td&gt;
&lt;td&gt;Carries the bulk of the fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch&lt;/td&gt;
&lt;td&gt;8-port 2.5GbE + 10GbE SFP+ unmanaged&lt;/td&gt;
&lt;td&gt;The faster segment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access Points&lt;/td&gt;
&lt;td&gt;4× TP-Link Deco&lt;/td&gt;
&lt;td&gt;Meshed Wi-Fi coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both switches are unmanaged, so the network is flat, no VLANs. Replacing them with managed switches is the next planned upgrade.&lt;/p&gt;
&lt;h2 id="expansion-headroom"&gt;Expansion Headroom
&lt;/h2&gt;&lt;p&gt;Worth knowing before planning any storage change: these are vendor specs; the OS reports something different (Linux claims eight SATA ports on all three nodes, including the one that physically has two).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;M.2 free&lt;/th&gt;
&lt;th&gt;SATA free&lt;/th&gt;
&lt;th&gt;PCIe slots&lt;/th&gt;
&lt;th&gt;DIMM slots free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pve-01&lt;/td&gt;
&lt;td&gt;1 of 3&lt;/td&gt;
&lt;td&gt;2 of 8&lt;/td&gt;
&lt;td&gt;3× x16 (one holds the HBA), 2× x1&lt;/td&gt;
&lt;td&gt;0 of 4 (64GB of a 128GB max)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pve-02&lt;/td&gt;
&lt;td&gt;1 of 2&lt;/td&gt;
&lt;td&gt;0 of 2&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0 of 2 (64GB of a 64GB max)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pve-03&lt;/td&gt;
&lt;td&gt;0 of 3&lt;/td&gt;
&lt;td&gt;4 of 4&lt;/td&gt;
&lt;td&gt;2× x16 (one holds the HBA), 2× x1&lt;/td&gt;
&lt;td&gt;0 of 4 (64GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every node is out of memory slots, so any RAM increase anywhere in the fleet means throwing away working DIMMs, and on pve-02 not even that helps, since it&amp;rsquo;s already at the board&amp;rsquo;s ceiling. pve-03 can&amp;rsquo;t take another NVMe without evicting one either, which is why the second backup datastore mirrors the two idle drives that were already in it rather than a new stick.&lt;/p&gt;
&lt;h2 id="rack--power"&gt;Rack &amp;amp; Power
&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rack&lt;/td&gt;
&lt;td&gt;Self-built 16U wooden rack. Everything mounts in it except pve-02, which sits on top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPS&lt;/td&gt;
&lt;td&gt;None (yet), utility power here is stable enough that I&amp;rsquo;ve accepted the risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description></item><item><title>Proxmox Idmapped Mount &amp; Permission Generator</title><link>https://diymediaserver.com/page/tools/proxmox-idmapped-mount/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/proxmox-idmapped-mount/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/proxmox-idmapped-mount/featured_hu_285c8356fc94b2c4.webp" alt="Featured image of post Proxmox Idmapped Mount &amp; Permission Generator" /&gt;&lt;p&gt;Enter your container ID, mount point details, and the host UID/GID you want to share. The tool generates the exact &lt;code&gt;mpX:&lt;/code&gt; config line for &lt;code&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;, the command to create the directory inside the container, and a host ownership check, all ready to copy.&lt;/p&gt;
&lt;p&gt;Idmapped mounts (available in Proxmox 8.1+ on kernel 6.x) let an unprivileged container see host files as if it owns them, without editing &lt;code&gt;/etc/subuid&lt;/code&gt; or &lt;code&gt;/etc/subgid&lt;/code&gt;. If you need selective pass-through of specific UID/GID ranges, use the &lt;a class="link" href="https://diymediaserver.com/page/tools/proxmox-lxc-idmap/" &gt;LXC UID/GID Mapping Tool&lt;/a&gt; instead.&lt;/p&gt;
&lt;div class="proxmox-idmapped-mount-tool" data-proxmox-idmapped-mount-tool="0"&gt;
&lt;form class="proxmox-idmapped-mount-form" autocomplete="off"&gt;
&lt;fieldset class="proxmox-idmapped-mount-inputs"&gt;
&lt;legend&gt;Mount settings&lt;/legend&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-ctid-0"&gt;Container ID (CTID)&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;The numeric ID of your LXC container, e.g. 101.&lt;/p&gt;
&lt;input type="number" id="proxmox-idmapped-mount-ctid-0"
min="100" max="999999999" placeholder="e.g. 101"
data-proxmox-idmapped-mount="ctid"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-mp-0"&gt;Mount Point ID&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;The &lt;code&gt;mpX&lt;/code&gt; index in the container config, e.g. &lt;code&gt;mp0&lt;/code&gt;.&lt;/p&gt;
&lt;input type="number" id="proxmox-idmapped-mount-mp-0"
min="0" max="255" placeholder="0" value="0"
data-proxmox-idmapped-mount="mp"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-host-path-0"&gt;Source (Host Path)&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;Absolute path to the directory on the Proxmox host, e.g. &lt;code&gt;/mnt/media&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmapped-mount-host-path-0"
placeholder="e.g. /mnt/media"
data-proxmox-idmapped-mount="host-path"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-lxc-path-0"&gt;Destination (LXC Path)&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;Absolute path where the data should appear inside the container, e.g. &lt;code&gt;/media&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmapped-mount-lxc-path-0"
placeholder="e.g. /media"
data-proxmox-idmapped-mount="lxc-path"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-uids-0"&gt;Host UID(s)&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;Numeric UIDs that own the host data. Used to generate the ownership check. Find with &lt;code&gt;stat -c "%u" /path&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmapped-mount-uids-0"
placeholder="e.g. 1000"
data-proxmox-idmapped-mount="uids"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-field"&gt;
&lt;label for="proxmox-idmapped-mount-gids-0"&gt;Host GID(s)&lt;/label&gt;
&lt;p class="proxmox-idmapped-mount-help"&gt;Numeric GIDs that own the host data. Often matches the UID. Find with &lt;code&gt;stat -c "%g" /path&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmapped-mount-gids-0"
placeholder="e.g. 1000"
data-proxmox-idmapped-mount="gids"&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="proxmox-idmapped-mount-outputs"&gt;
&lt;div class="proxmox-idmapped-mount-output-block"&gt;
&lt;div class="proxmox-idmapped-mount-output-header"&gt;
&lt;span class="proxmox-idmapped-mount-output-title"&gt;&lt;code data-proxmox-idmapped-mount-conftitle&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="proxmox-idmapped-mount-copy" data-target="proxmox-idmapped-mount-config-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="proxmox-idmapped-mount-config-0"&gt;Enter a CTID, host path, and LXC path to generate the config line.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-output-block"&gt;
&lt;div class="proxmox-idmapped-mount-output-header"&gt;
&lt;span class="proxmox-idmapped-mount-output-title"&gt;Create directory inside container&lt;/span&gt;
&lt;button type="button" class="proxmox-idmapped-mount-copy" data-target="proxmox-idmapped-mount-mkdir-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="proxmox-idmapped-mount-mkdir-0"&gt;Enter a CTID and LXC path to generate the mkdir command.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmapped-mount-output-block"&gt;
&lt;div class="proxmox-idmapped-mount-output-header"&gt;
&lt;span class="proxmox-idmapped-mount-output-title"&gt;Verify host ownership&lt;/span&gt;
&lt;button type="button" class="proxmox-idmapped-mount-copy" data-target="proxmox-idmapped-mount-verify-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="proxmox-idmapped-mount-verify-0"&gt;Enter a host path to generate the verification command.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="the-permission-bridge"&gt;The Permission Bridge
&lt;/h2&gt;&lt;h3 id="step-1-identify-host-owners"&gt;Step 1: Identify Host Owners
&lt;/h3&gt;&lt;p&gt;Find the numeric UID and GID that own your data on the Proxmox host:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stat -c &lt;span class="s2"&gt;&amp;#34;%u %g&amp;#34;&lt;/span&gt; /path/to/host/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The two numbers in the output are the UID and GID. Enter them into the tool above.&lt;/p&gt;
&lt;h3 id="step-2-identify-container-users"&gt;Step 2: Identify Container Users
&lt;/h3&gt;&lt;p&gt;Look up the service-account ID inside the container (e.g. the user running Jellyfin or Sonarr):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; CTID -- id -u username
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pct &lt;span class="nb"&gt;exec&lt;/span&gt; CTID -- id -g username
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The UID and GID printed here are what the container process expects to run as. The idmapped mount makes the host files appear to be owned by those IDs at the kernel level.&lt;/p&gt;
&lt;h3 id="compatibility-notes"&gt;Compatibility Notes
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Requires Proxmox 8.1 or newer (kernel 6.x or higher).&lt;/li&gt;
&lt;li&gt;Backing filesystem must support idmapped mounts: ZFS, ext4, XFS. Some legacy network mounts (older NFS/CIFS exports) do not support this feature.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_db44a65f7d14b52f.avif"&gt;&lt;img src="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_8abc5b45895fa4b2.webp" width="100" height="100" alt="Mastering UID/GID Mapping in Proxmox LXC Containers" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;For the conceptual background on UID/GID mapping, default offset layouts, and when traditional &lt;code&gt;lxc.idmap&lt;/code&gt; is still the right call.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/"
class="backlink-button"
target="_self"
&gt;
Read the full guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Proxmox LXC idmap → Idmapped Mount Converter</title><link>https://diymediaserver.com/page/tools/proxmox-idmap-converter/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/proxmox-idmap-converter/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/proxmox-idmap-converter/featured_hu_a60a40837ae43924.webp" alt="Featured image of post Proxmox LXC idmap → Idmapped Mount Converter" /&gt;&lt;p&gt;Paste the &lt;code&gt;lxc.idmap&lt;/code&gt; lines from your container config, enter the host path, LXC path, and mount point index. The tool detects which UID/GID rows are passthrough mappings, generates the replacement &lt;code&gt;mpX:&lt;/code&gt; line for &lt;code&gt;/etc/pve/lxc/&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;, and shows the cleanup commands to remove the old &lt;code&gt;lxc.idmap&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;Requires Proxmox 8.1 or newer (kernel 6.x). The backing filesystem must support idmapped mounts: ZFS, ext4, and XFS all work. Older NFS or CIFS exports may not.&lt;/p&gt;
&lt;div class="proxmox-idmap-converter-tool" data-proxmox-idmap-converter-tool="0"&gt;
&lt;form class="proxmox-idmap-converter-form" autocomplete="off"&gt;
&lt;fieldset class="proxmox-idmap-converter-inputs"&gt;
&lt;legend&gt;Old config&lt;/legend&gt;
&lt;div class="proxmox-idmap-converter-field"&gt;
&lt;label for="proxmox-idmap-converter-idmap-0"&gt;Old &lt;code&gt;lxc.idmap&lt;/code&gt; block&lt;/label&gt;
&lt;p class="proxmox-idmap-converter-help"&gt;Paste the &lt;code&gt;lxc.idmap&lt;/code&gt; lines from your &lt;code&gt;&amp;lt;CTID&amp;gt;.conf&lt;/code&gt;. One line per row.&lt;/p&gt;
&lt;textarea id="proxmox-idmap-converter-idmap-0"
rows="6"
placeholder="lxc.idmap: u 0 100000 1000&amp;#10;lxc.idmap: g 0 100000 1000&amp;#10;lxc.idmap: u 1000 1000 1&amp;#10;lxc.idmap: g 1000 1000 1&amp;#10;lxc.idmap: u 1001 101001 64535&amp;#10;lxc.idmap: g 1001 101001 64535"
data-proxmox-idmap-converter="idmap"&gt;&lt;/textarea&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-field"&gt;
&lt;label for="proxmox-idmap-converter-host-path-0"&gt;Host path&lt;/label&gt;
&lt;p class="proxmox-idmap-converter-help"&gt;Absolute path to the directory on the Proxmox host, e.g. &lt;code&gt;/mnt/media&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmap-converter-host-path-0"
placeholder="e.g. /mnt/media"
data-proxmox-idmap-converter="host-path"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-field"&gt;
&lt;label for="proxmox-idmap-converter-lxc-path-0"&gt;LXC path&lt;/label&gt;
&lt;p class="proxmox-idmap-converter-help"&gt;Absolute path where the data should appear inside the container, e.g. &lt;code&gt;/media&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="proxmox-idmap-converter-lxc-path-0"
placeholder="e.g. /media"
data-proxmox-idmap-converter="lxc-path"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-field"&gt;
&lt;label for="proxmox-idmap-converter-mp-0"&gt;Mount point index&lt;/label&gt;
&lt;p class="proxmox-idmap-converter-help"&gt;The &lt;code&gt;mpX&lt;/code&gt; index in the container config, e.g. &lt;code&gt;0&lt;/code&gt; for &lt;code&gt;mp0&lt;/code&gt;.&lt;/p&gt;
&lt;input type="number" id="proxmox-idmap-converter-mp-0"
min="0" max="255" placeholder="0" value="0"
data-proxmox-idmap-converter="mp"&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-field"&gt;
&lt;label for="proxmox-idmap-converter-ctid-0"&gt;Container ID (optional)&lt;/label&gt;
&lt;p class="proxmox-idmap-converter-help"&gt;Used only in the cleanup commands. Leave blank to use a generic &lt;code&gt;&amp;lt;CTID&amp;gt;&lt;/code&gt; placeholder.&lt;/p&gt;
&lt;input type="number" id="proxmox-idmap-converter-ctid-0"
min="100" max="999999999" placeholder="e.g. 100"
data-proxmox-idmap-converter="ctid"&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="proxmox-idmap-converter-outputs"&gt;
&lt;div class="proxmox-idmap-converter-output-block"&gt;
&lt;div class="proxmox-idmap-converter-output-header"&gt;
&lt;span class="proxmox-idmap-converter-output-title"&gt;Detected passthrough IDs&lt;/span&gt;
&lt;button type="button" class="proxmox-idmap-converter-copy" data-target="proxmox-idmap-converter-ids-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code id="proxmox-idmap-converter-ids-0"&gt;No passthrough rows detected — paste your lxc.idmap block above.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-output-block"&gt;
&lt;div class="proxmox-idmap-converter-output-header"&gt;
&lt;span class="proxmox-idmap-converter-output-title"&gt;New &lt;code data-proxmox-idmap-converter-conftitle&gt;&amp;lt;CTID&amp;gt;.conf&lt;/code&gt; line&lt;/span&gt;
&lt;button type="button" class="proxmox-idmap-converter-copy" data-target="proxmox-idmap-converter-config-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="proxmox-idmap-converter-config-0"&gt;Enter a host path and LXC path to generate the config line.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="proxmox-idmap-converter-output-block"&gt;
&lt;div class="proxmox-idmap-converter-output-header"&gt;
&lt;span class="proxmox-idmap-converter-output-title"&gt;Cleanup commands&lt;/span&gt;
&lt;button type="button" class="proxmox-idmap-converter-copy" data-target="proxmox-idmap-converter-cleanup-0" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" id="proxmox-idmap-converter-cleanup-0"&gt;Fill in the fields above to generate cleanup commands.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="what-this-tool-does"&gt;What this tool does
&lt;/h2&gt;&lt;p&gt;Legacy LXC containers on Proxmox used a &lt;code&gt;lxc.idmap&lt;/code&gt; block in &lt;code&gt;&amp;lt;CTID&amp;gt;.conf&lt;/code&gt; plus matching entries in &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt; to remap container UIDs/GIDs to host UIDs/GIDs. A typical setup for passing through a single host user (UID 1000) looks like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lxc.idmap: u 0 100000 1000
lxc.idmap: g 0 100000 1000
lxc.idmap: u 1000 1000 1
lxc.idmap: g 1000 1000 1
lxc.idmap: u 1001 101001 64535
lxc.idmap: g 1001 101001 64535
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The tool reads those lines, picks out the passthrough rows (&lt;code&gt;containerId === hostId&lt;/code&gt;), and builds the idmapped mount line that replaces them: &lt;code&gt;mp0: /mnt/media,mp=/media,idmapped=1&lt;/code&gt;. It also shows the &lt;code&gt;sed&lt;/code&gt; command to strip the old &lt;code&gt;lxc.idmap&lt;/code&gt; lines and a reminder to clean up the matching &lt;code&gt;subuid&lt;/code&gt;/&lt;code&gt;subgid&lt;/code&gt; entries.&lt;/p&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_db44a65f7d14b52f.avif"&gt;&lt;img src="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/featured_hu_8abc5b45895fa4b2.webp" width="100" height="100" alt="Mastering UID/GID Mapping in Proxmox LXC Containers" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Background on UID/GID mapping, default offset layouts, and when traditional &lt;code&gt;lxc.idmap&lt;/code&gt; is still the right approach.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/mastering-uid-gid-mapping-proxmox-lxc/"
class="backlink-button"
target="_self"
&gt;
Read the full guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item><item><title>Search</title><link>https://diymediaserver.com/page/search/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/search/</guid><description/></item><item><title>SnapRAID Parity &amp; MergerFS Pool Calculator</title><link>https://diymediaserver.com/page/tools/snapraid-calculator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://diymediaserver.com/page/tools/snapraid-calculator/</guid><description>&lt;img src="https://diymediaserver.com/page/tools/snapraid-calculator/featured_hu_d810565892a5cae.webp" alt="Featured image of post SnapRAID Parity &amp; MergerFS Pool Calculator" /&gt;&lt;p&gt;Enter the sizes of your data drives (and parity drives, if you already own them). The calculator tells you how many parity drives you need and how big they must be, then generates the &lt;code&gt;snapraid.conf&lt;/code&gt; and the MergerFS &lt;code&gt;/etc/fstab&lt;/code&gt; line to pool the data drives.&lt;/p&gt;
&lt;div class="hb-tool" data-snapraid-calculator&gt;
&lt;form class="hb-tool-form" autocomplete="off"&gt;
&lt;fieldset class="hb-tool-inputs"&gt;
&lt;legend&gt;Your drives&lt;/legend&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="snapraid-data-sizes-0"&gt;Data drive sizes (TB)&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Comma or space separated, one number per drive, e.g. &lt;code&gt;8, 8, 4, 12&lt;/code&gt;. Decimals are fine.&lt;/p&gt;
&lt;input type="text" id="snapraid-data-sizes-0"
placeholder="e.g. 8, 8, 4, 12"
data-field="data-sizes"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="snapraid-parity-sizes-0"&gt;Parity drive sizes (TB) — optional&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Leave blank if you haven't bought parity drives yet — the tool tells you what to buy.&lt;/p&gt;
&lt;input type="text" id="snapraid-parity-sizes-0"
placeholder="e.g. 12"
data-field="parity-sizes"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="snapraid-data-base-0"&gt;Data drive mount prefix&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Data drives mount as &lt;code&gt;&amp;lt;prefix&amp;gt;1&lt;/code&gt;, &lt;code&gt;&amp;lt;prefix&amp;gt;2&lt;/code&gt;, … The MergerFS pool globs &lt;code&gt;&amp;lt;prefix&amp;gt;*&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="snapraid-data-base-0"
placeholder="/mnt/pool/disk" value="/mnt/pool/disk"
data-field="data-base"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="snapraid-parity-base-0"&gt;Parity drive mount prefix&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Kept outside the data glob so parity never joins the MergerFS pool.&lt;/p&gt;
&lt;input type="text" id="snapraid-parity-base-0"
placeholder="/mnt/pool/parity" value="/mnt/pool/parity"
data-field="parity-base"&gt;
&lt;/div&gt;
&lt;div class="hb-tool-field"&gt;
&lt;label for="snapraid-pool-0"&gt;Pool mount point&lt;/label&gt;
&lt;p class="hb-tool-help"&gt;Where the combined MergerFS pool appears, e.g. &lt;code&gt;/mnt/storage&lt;/code&gt;.&lt;/p&gt;
&lt;input type="text" id="snapraid-pool-0"
placeholder="/mnt/storage" value="/mnt/storage"
data-field="pool"&gt;
&lt;/div&gt;
&lt;/fieldset&gt;
&lt;/form&gt;
&lt;div class="hb-tool-outputs"&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;Array summary&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="summary" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code data-output="summary"&gt;Enter your data drive sizes to size the array.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;&lt;code&gt;/etc/snapraid.conf&lt;/code&gt;&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="conf" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" data-output="conf"&gt;Enter your data drive sizes to generate snapraid.conf.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="hb-tool-output-block"&gt;
&lt;div class="hb-tool-output-header"&gt;
&lt;span class="hb-tool-output-title"&gt;&lt;code&gt;/etc/fstab&lt;/code&gt; MergerFS pool line&lt;/span&gt;
&lt;button type="button" class="hb-tool-copy" data-copy="fstab" disabled&gt;Copy&lt;/button&gt;
&lt;/div&gt;
&lt;pre aria-live="polite"&gt;&lt;code class="language-bash" data-output="fstab"&gt;Enter your data drive sizes to generate the fstab line.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="how-the-parity-recommendation-works"&gt;How the parity recommendation works
&lt;/h2&gt;&lt;p&gt;The parity counts follow the official SnapRAID FAQ guideline:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data drives&lt;/th&gt;
&lt;th&gt;Parity drives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2–4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–14&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15–21&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22–28&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29–35&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36–42&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each parity drive protects against one simultaneous drive failure. The one hard rule: &lt;strong&gt;every parity drive must be at least as large as your largest data drive.&lt;/strong&gt; If you&amp;rsquo;re buying drives, it&amp;rsquo;s common to buy one size up for parity so future data-drive upgrades don&amp;rsquo;t force a parity upgrade too.&lt;/p&gt;
&lt;p&gt;Unlike traditional RAID, SnapRAID parity costs you no capacity on the data drives: usable space is the sum of your data drives, and each file lives whole on one disk.&lt;/p&gt;
&lt;h2 id="why-parity-mounts-at-mntpoolparity-not-mntpooldisk"&gt;Why parity mounts at /mnt/pool/parity, not /mnt/pool/disk
&lt;/h2&gt;&lt;p&gt;The generated fstab line pools every mount matching &lt;code&gt;/mnt/pool/disk*&lt;/code&gt; into MergerFS. If a parity drive were mounted at &lt;code&gt;/mnt/pool/disk4&lt;/code&gt;, the glob would swallow it into the pool and your media apps would happily fill the parity drive with files. Mounting parity under a different prefix (&lt;code&gt;/mnt/pool/parity1&lt;/code&gt;) keeps it out of the pool by construction.&lt;/p&gt;
&lt;h2 id="after-you-copy-the-config"&gt;After you copy the config
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Partition, format, and mount each drive first (the mounts referenced in &lt;code&gt;snapraid.conf&lt;/code&gt; and fstab must exist).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;snapraid sync&lt;/code&gt; to build parity, then schedule regular syncs and scrubs.&lt;/li&gt;
&lt;li&gt;Reload mounts with &lt;code&gt;sudo systemctl daemon-reload &amp;amp;&amp;amp; sudo mount -a&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/snapraid2_hu_19ed13048fd2f851.avif"&gt;&lt;img src="https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/snapraid2_hu_9a0a172fa6833060.webp" width="100" height="100" alt="Master the Basics: How to Install SnapRAID" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Full walkthrough of installing SnapRAID, what parity and content files actually do, and the sync/scrub routine that keeps the array healthy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/master-the-basics-how-to-install-snapraid/"
class="backlink-button"
target="_self"
&gt;
SnapRAID install guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="backlink-box"&gt;
&lt;div class="backlink-box-image"&gt;&lt;picture&gt;&lt;source type="image/avif" srcset="https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/featured_hu_a50a811615b1e898.avif"&gt;&lt;img src="https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/featured_hu_fc62802c5236d86e.webp" width="100" height="100" alt="Build Your Own NAS Storage with MergerFS (2026 Guide)" loading="lazy" decoding="async"&gt;&lt;/picture&gt;&lt;/div&gt;
&lt;div class="backlink-box-content"&gt;
&lt;p&gt;Where the fstab line above comes from: what each mount option does, and how to add a cache drive tier later.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="backlink-box-links"&gt;
&lt;a href="https://diymediaserver.com/post/2026/mergerfs-media-servers-2026/"
class="backlink-button"
target="_self"
&gt;
MergerFS 2026 guide
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;</description></item></channel></rss>