<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[First Homelab and Chaos]]></title><description><![CDATA[A practical field guide to setting up self-hosted services, securing Ubuntu servers with Tailscale while tackling through errors and oversights.]]></description><link>https://first-homelab-setup.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>First Homelab and Chaos</title><link>https://first-homelab-setup.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 10 Sep 2026 22:26:15 GMT</lastBuildDate><atom:link href="https://first-homelab-setup.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Tips for Building and Debugging a Secure Ubuntu Homelab with Tailscale & Docker
]]></title><description><![CDATA[Setting up a home server is a rite of passage for any developer. But there is a massive difference between getting a server to ping on your local network and building a secure, easily manageable envir]]></description><link>https://first-homelab-setup.hashnode.dev/tips-for-building-and-debugging-a-secure-ubuntu-homelab-with-tailscale-docker</link><guid isPermaLink="true">https://first-homelab-setup.hashnode.dev/tips-for-building-and-debugging-a-secure-ubuntu-homelab-with-tailscale-docker</guid><category><![CDATA[Homelab]]></category><category><![CDATA[secureserver]]></category><category><![CDATA[Docker]]></category><category><![CDATA[server]]></category><category><![CDATA[tailscale]]></category><category><![CDATA[zerotrust]]></category><category><![CDATA[DIY]]></category><category><![CDATA[ssh]]></category><category><![CDATA[tailnet]]></category><category><![CDATA[cybersecurity]]></category><category><![CDATA[headless]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Walid Hassan Tahrim]]></dc:creator><pubDate>Thu, 03 Sep 2026 19:41:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a31e39c71976423dea66d2a/150a980e-9276-474a-a43a-fcdaeb9256c4.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Setting up a home server is a rite of passage for any developer. But there is a massive difference between getting a server to ping on your local network and building a secure, easily manageable environment you can access from anywhere in the world.</p>
<p>Recently, I set out to build a robust Ubuntu homelab. My goals were simple: containerize everything with Docker, lock down external access, and use Tailscale to create a secure, zero-trust mesh VPN.</p>
<p>Of course, the reality of homelabbing is that things <em>will</em> break. What follows is some of my tips to setting up this architecture—and some debugging logs of exactly how I fixed it when things went sideways.</p>
<hr />
<h2>Part 1: The Architecture &amp; Setup Cheat Sheet</h2>
<p>Before we can run our services, we need a solid foundation. Here are the core steps I took to prep the Ubuntu server and lock down the network.</p>
<h3>1. Reclaiming Storage (LVM Expansion)</h3>
<p>By default, Ubuntu Server often limits the root partition to just 100GB, leaving the rest of your SSD unallocated. Before deploying heavy containers, I had to expand the Logical Volume (LVM) to utilize the full physical disk. If you are starting fresh, always check your <code>df -h</code> output first!</p>
<h3>2. The Great Static IP Debate</h3>
<p>I needed the server's local IP (<code>192.168.1.51</code>) to remain permanent. While it is tempting to hardcode this directly into Ubuntu's Netplan, I opted to manage it entirely via the <strong>Router UI (Static DHCP)</strong>. Tying the IP to the server's MAC address at the router level is significantly safer and prevents network conflicts if you ever move the machine.</p>
<h3>3. Punching the Tailscale Firewall Hole</h3>
<p>With Tailscale installed, the server is accessible via the mesh VPN, but Ubuntu's Uncomplicated Firewall (UFW) will still block the traffic. You need to explicitly tell UFW to trust the Tailscale interface:</p>
<pre><code class="language-bash">sudo ufw allow in on tailscale0
sudo ufw allow 9443/tcp
</code></pre>
<h3>4. Installing Docker (The Pro Way)</h3>
<p>Always install via the official Docker repository, not the snap package. More importantly, add your user to the Docker admin group so you aren't forced to type <code>sudo</code> before every single container command:</p>
<pre><code class="language-bash">sudo usermod -aG docker sysadmin
</code></pre>
<h3>5. Deploying Portainer with Tailscale SSL</h3>
<p>I use Portainer to manage my Docker environments. To access it securely over HTTPS without annoying browser warnings, I provisioned a free SSL certificate using Tailscale's MagicDNS, secured the keys, and mounted them directly into the container:</p>
<pre><code class="language-bash"># 1. Generate the certs (Replace tailXXXXXX with your Tailnet alias)
sudo tailscale cert homelab-node1.tailXXXXXX.ts.net

# 2. Move and secure them permanently
sudo mkdir -p /etc/portainer/certs
sudo mv *.ts.net.* /etc/portainer/certs/
sudo chmod -R 755 /etc/portainer/certs

# 3. Create Portainer data save-state
docker volume create portainer_data

# 4. Launch Portainer 
docker run -d -p 8000:8000 -p 9443:9443 --name portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  -v /etc/portainer/certs:/certs portainer/portainer-ce:latest \
  --sslcert /certs/homelab-node1.tailXXXXXX.ts.net.crt \
  --sslkey /certs/homelab-node1.tailXXXXXX.ts.net.key
</code></pre>
<h3>6. Network Optimization (UDP GRO)</h3>
<p>Tailscale relies heavily on UDP traffic. On standard Linux setups, this can become a serious performance bottleneck. I implemented a network tuning optimization using <code>ethtool</code> to enable UDP Generic Receive Offload (GRO). To ensure it survived a reboot, I baked it into a custom systemd service (<code>optimize-network.service</code>).</p>
<h3>7. Securing Local AI Models (Ollama)</h3>
<p>If you are running local AI models on a VPN-connected server, you do not want those endpoints exposed indiscriminately. I ensured Ollama (Port <code>11434</code>) was explicitly bound to <code>127.0.0.1</code> (localhost) rather than <code>0.0.0.0</code>. This guarantees it is completely invisible to the outside network and can only be accessed via secure tunneling.</p>
<h3>8. The "Friend" Access (Tailscale Node Sharing)</h3>
<p>I wanted to give a friend access to a specific web app (Port 3000) without giving them full SSH access or Exit Node routing capabilities. The fix? Tailscale's "Share" feature. I left the "Allow use as exit node" box <em>unchecked</em> and wrote a strict Access Control List (ACL) rule to only accept traffic from their email to that specific port:</p>
<pre><code class="language-json">{ "action": "accept", "src": ["friend@email.com"], "dst": ["homelab-node1:3000"] }
</code></pre>
<hr />
<h2>Part 2: The Debugging Logs</h2>
<p>Theory is great, but here is what actually happened when things broke, and the exact steps I took to fix them.</p>
<h3>🐛 Error 1: Portainer's <code>ERR_CONNECTION_REFUSED</code> Crash Loop</h3>
<p><strong>The Symptom:</strong> After deploying Portainer with SSL, the browser actively rejected the connection on port 9443. <strong>The Diagnosis:</strong> Running <code>docker ps</code> revealed the container status was <code>Restarting (1) 20 seconds ago</code>. It was in a crash loop. <strong>The Fix:</strong> Portainer was panicking because it couldn't read the certificate files. I had to stop relying on wildcards and temporary folders. I built a permanent directory (<code>/etc/portainer/certs</code>), fixed the Linux user permissions (<code>chmod -R 755</code>), and relaunched the container with explicit, hardcoded file paths.</p>
<h3>🐛 Error 2: The Cross-OS File Upload Trap</h3>
<p><strong>The Symptom:</strong> Portainer’s web UI asked me to manually upload the <code>.crt</code> and <code>.key</code> files. <strong>The Root Cause:</strong> I was accessing the UI from Windows, but the certificates were generated and stored on the headless Ubuntu server. Windows couldn't "see" inside the Linux drive to upload them. <strong>The Fix:</strong> Bypassed the web UI entirely. By using Docker volume mapping (<code>-v</code>), I mounted the Ubuntu folder containing the certs directly into Portainer’s backend. Problem solved.</p>
<h3>🐛 Error 3: Fighting the Tailscale CLI</h3>
<p><strong>The Symptom:</strong> Commands like <code>tailscale ip --dns</code> returned "flag provided but not defined" errors. <strong>The Fix (and Lesson):</strong> Tailscale’s CLI is highly locked down and truncates output to keep terminal tables clean. Stop fighting the terminal for domain names! The fastest way to check your exact MagicDNS URLs is simply looking at the Tailscale Web Admin Console.</p>
<h3>🐛 Error 4: Router Warnings on Static IPs</h3>
<p><strong>The Symptom:</strong> Setting the Static IP (<code>192.168.1.51</code>) threw a router warning: *"Need to disconnect/connect the device or release/renew IP."*<strong>The Root Cause:</strong> The router wrote the rule, but the server still had the old temporary network lease active in its RAM. <strong>The Fix:</strong> Ignored the warning, gracefully powered off the server (<code>sudo poweroff</code>), and booted it back up. The fresh network handshake seamlessly locked in the permanent IP.</p>
<h3>🐛 Error 5: The Windows VPN Crash Loop (<code>unexpected state: NoState</code>)</h3>
<p><strong>The Symptom:</strong> SSH timed out. Running <code>tailscale status</code> on Windows returned <code>unexpected state: NoState</code> and <code>context canceled</code>. <strong>The Root Cause:</strong> The Windows Tailscale daemon lost its cryptographic identity (often triggered by network drops or sleep/wake desyncs). Standard restarts did nothing. <strong>The Fix:</strong> I opened an <strong>Administrator PowerShell</strong>, force-stopped the service, renamed the corrupted state file (<code>C:\ProgramData\Tailscale\server-state.conf</code>), and forced a fresh login via <code>tailscale up --force-reauth</code>.</p>
<h3>🐛 Error 6: The Shared Node IP Rejection</h3>
<p><strong>The Symptom:</strong> My friend accepted the Node Share invite but couldn't reach the app using the raw Tailscale IP (<code>http://100.X.X.X:3000</code>). In hindsight, he should've known better than to try dumb sh*t like this 😂. <strong>The Root Cause:</strong> Tailscale intentionally blocks shared users from navigating via raw <code>100.x.x.x</code> IPs to prevent IP collisions across merged Tailnets. <strong>The Fix:</strong> Instructed my friend to drop the IP address completely and exclusively use the MagicDNS hostname (<code>http://homelab-node1.tailXXXXXX.ts.net:3000</code>).</p>
<hr />
<h3>Conclusion</h3>
<p>A homelab is never truly "finished," but combining Docker's containerization with Tailscale's zero-trust networking creates an incredibly resilient environment. When things break (and they will), don't panic—check the logs, verify your network routes, and keep building.</p>
<p><em>What are you hosting in your homelab right now? Let me know in the comments!</em></p>
]]></content:encoded></item></channel></rss>