<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike</title>
    <description>The latest articles on DEV Community by Mike (@techiemike).</description>
    <link>https://dev.to/techiemike</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962837%2Fd01490d0-dea6-4e9f-b226-ea0ef2357113.png</url>
      <title>DEV Community: Mike</title>
      <link>https://dev.to/techiemike</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techiemike"/>
    <language>en</language>
    <item>
      <title>GitLost: How AI Agents Can Leak Your Code Through a Single GitHub Issue</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Fri, 17 Jul 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/techiemike/gitlost-how-ai-agents-can-leak-your-code-through-a-single-github-issue-53f0</link>
      <guid>https://dev.to/techiemike/gitlost-how-ai-agents-can-leak-your-code-through-a-single-github-issue-53f0</guid>
      <description>&lt;h1&gt;
  
  
  GitLost: How AI Agents Can Leak Your Code Through a Single GitHub Issue
&lt;/h1&gt;

&lt;p&gt;A public GitHub issue should not be able to pull text out of your private repositories.&lt;/p&gt;

&lt;p&gt;With a dangerously over-permissioned GitHub Agentic Workflow, that is exactly what Noma Labs demonstrated.&lt;/p&gt;

&lt;p&gt;Their July 2026 write-up showed a simple but nasty chain: an attacker opens an issue in a public repository, an AI agent reads it as part of an automated workflow, the agent reaches into other repositories it can access, and the results come back as a public comment. No stolen login. No insider access. No clever memory corruption. Just a workflow that trusted the wrong text.&lt;/p&gt;

&lt;p&gt;I run Hermes agents against GitHub-adjacent workflows on my own kit, and this is the class of bug I worry about far more than model benchmark scores. Once an agent can read one place, write somewhere else, and decide its own next step in between, your problem is no longer "AI output quality." Your problem is access control.&lt;/p&gt;

&lt;p&gt;My view is blunt: if an agent can read private code and post publicly in the same run, the design is already wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitLost actually showed
&lt;/h2&gt;

&lt;p&gt;Noma Labs published GitLost on 6 July 2026. The attack targeted GitHub's new agentic workflow pattern: Markdown-defined workflows tied to GitHub Actions, with an AI agent reading issues, calling tools, and replying automatically.&lt;/p&gt;

&lt;p&gt;The dangerous part was not "AI" in the abstract. It was the permission mix.&lt;/p&gt;

&lt;p&gt;According to Noma's write-up, the vulnerable pattern looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a public repository in the same GitHub organization as private repositories&lt;/li&gt;
&lt;li&gt;an automated workflow triggered by issue activity such as &lt;code&gt;issues.assigned&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;an agent that reads the issue title and body&lt;/li&gt;
&lt;li&gt;permission for that agent to read other repositories in the organization&lt;/li&gt;
&lt;li&gt;permission for that same agent to post back into the public issue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is enough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0w53she5akdu2hqdrob.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0w53she5akdu2hqdrob.webp" alt="GitLost attack flow showing a public GitHub issue steering an AI agent to read a private repository and post the contents back into a public comment" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Noma's proof of concept showed the agent fetching &lt;code&gt;README.md&lt;/code&gt; from both a public repo and a private repo, then posting the contents back into a public comment thread where anyone could read them. Their write-up says the leaked files came from repositories named &lt;code&gt;poc&lt;/code&gt; and &lt;code&gt;testlocal&lt;/code&gt;, and that the workflow was triggered after the public issue was assigned.&lt;/p&gt;

&lt;p&gt;That matters because it turns a boring workflow event into a trust-boundary failure.&lt;/p&gt;

&lt;p&gt;A lot of people still hear "prompt injection" and imagine someone typing "ignore previous instructions" into ChatGPT for fun. This is not that. This is a live automation path with real repository permissions attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack is simple because the system is over-trusted
&lt;/h2&gt;

&lt;p&gt;The rough sequence looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An attacker opens an issue in a public repository.&lt;/li&gt;
&lt;li&gt;The issue looks harmless enough to survive normal human scanning.&lt;/li&gt;
&lt;li&gt;The workflow assigns or otherwise triggers the agent.&lt;/li&gt;
&lt;li&gt;The agent reads the issue body as part of its input.&lt;/li&gt;
&lt;li&gt;Hidden instructions in that issue push the agent to read other repositories and publish what it finds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Noma says one small wording change, adding the word "Additionally", was enough in their testing to push the model around GitHub's intended guardrails and make it reframe the answer instead of refusing it.&lt;/p&gt;

&lt;p&gt;That is the part people need to sit with.&lt;/p&gt;

&lt;p&gt;This was not a case of the attacker breaking GitHub authentication. The attacker used the workflow exactly as exposed. The workflow itself bridged public input and private data too freely.&lt;/p&gt;

&lt;p&gt;If you teach computer science, the classroom analogy is straightforward. This is like letting a student write a note on the whiteboard, having an assistant treat that note as an instruction sheet, then letting the same assistant walk into the locked filing cabinet and pin the contents back on the classroom door.&lt;/p&gt;

&lt;p&gt;The filing cabinet is not the weak point there. The procedure is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters even if you do not use GitHub's exact setup
&lt;/h2&gt;

&lt;p&gt;Most people will read GitLost and ask, "Am I affected if I do not use GitHub Agentic Workflows?"&lt;/p&gt;

&lt;p&gt;Maybe not by this exact proof of concept.&lt;/p&gt;

&lt;p&gt;But the broader lesson applies to every agent stack, including self-hosted ones.&lt;/p&gt;

&lt;p&gt;I wrote recently about &lt;a href="https://www.techiemike.com/right-to-local-intelligence/" rel="noopener noreferrer"&gt;why local AI matters&lt;/a&gt; and about &lt;a href="https://www.techiemike.com/ollama-security-self-hosted-ai/" rel="noopener noreferrer"&gt;securing self-hosted Ollama&lt;/a&gt;. Running your models locally does help with privacy. It stops your prompts and code from automatically leaving for a cloud provider.&lt;/p&gt;

&lt;p&gt;It does not solve this problem.&lt;/p&gt;

&lt;p&gt;GitLost is not mainly a model-hosting problem. It is an orchestration problem.&lt;/p&gt;

&lt;p&gt;If your agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read public issues or comments&lt;/li&gt;
&lt;li&gt;access private repositories, secrets, or internal docs&lt;/li&gt;
&lt;li&gt;open pull requests, comments, emails, or tickets automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then you have built a system where untrusted text can influence trusted actions.&lt;/p&gt;

&lt;p&gt;That is the trust boundary that matters.&lt;/p&gt;

&lt;p&gt;I see the same misunderstanding in a lot of AI tool conversations. People obsess over which model is smartest and ignore which tool has the broadest token, the loosest repository scope, or the most dangerous default action. That is backwards. In practice, the model is often the least interesting part. I made the same argument in my post on &lt;a href="https://www.techiemike.com/multi-agent-debate-ai-architecture/" rel="noopener noreferrer"&gt;multi-agent debate and AI architecture&lt;/a&gt;: the intelligence people admire usually lives in the orchestration, and so do the failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real mistake: one agent, too many powers
&lt;/h2&gt;

&lt;p&gt;The cleanest way to think about GitLost is this: one actor in the system was allowed to do too many kinds of work.&lt;/p&gt;

&lt;p&gt;It could inspect untrusted public text.&lt;br&gt;
It could fetch private information.&lt;br&gt;
It could speak publicly.&lt;/p&gt;

&lt;p&gt;That is too much authority for one automated step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9px232d3x8gez3w4wy5e.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9px232d3x8gez3w4wy5e.webp" alt="Diagram showing safer AI agent permission design: separate agents or tokens for reading public issues, accessing private repositories, and posting externally" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If I gave a student account permission to read every exam paper, every answer sheet, and every staff note, I would not also let that same account publish messages to the whole class without review. Yet that is roughly what some agent workflows are doing.&lt;/p&gt;

&lt;p&gt;And this is why GitLost is a better lesson than a lot of headline-friendly AI panic. It exposes a design error you can actually fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five changes that reduce this risk fast
&lt;/h2&gt;

&lt;p&gt;None of these make an agent workflow "safe." That word is too absolute for security work. They do reduce the attack surface.&lt;/p&gt;

&lt;p&gt;GitHub Agentic Workflows are read-only by default and separate write operations through safe outputs, but those controls cannot compensate for an agent that has unnecessarily broad read access and is allowed to publish sensitive output.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Scope each agent to one repository whenever possible
&lt;/h3&gt;

&lt;p&gt;Cross-repository access is convenient, but it is also how a public issue in one repo turns into a leak from another.&lt;/p&gt;

&lt;p&gt;If a workflow only needs access to the repository where the issue was opened, give it only that. If a second private repository is genuinely required, make that an explicit separate step with its own token and its own review point.&lt;/p&gt;

&lt;p&gt;This is the same least-privilege logic that applies to any secret or service account. If the agent does not need the key, do not hand it the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Split public-writing steps away from private-reading steps
&lt;/h3&gt;

&lt;p&gt;This is the biggest architectural fix.&lt;/p&gt;

&lt;p&gt;Do not let the same workflow run both of these operations automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read internal or private material&lt;/li&gt;
&lt;li&gt;publish directly to a public thread&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need both, add a gate between them. Make the first step collect data into a private review channel. For sensitive workflows, require human approval before anything goes back out publicly.&lt;/p&gt;

&lt;p&gt;Yes, this slows the workflow down.&lt;/p&gt;

&lt;p&gt;Good.&lt;/p&gt;

&lt;p&gt;Security controls that cost nothing usually protect nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Treat issues, pull requests, and comments as untrusted input
&lt;/h3&gt;

&lt;p&gt;Every issue body, PR description, code comment, changelog, and Markdown file the agent reads should be treated as hostile until proven otherwise.&lt;/p&gt;

&lt;p&gt;That does not mean you stop using automation. It means you stop pretending user-controlled text is just data. In an agentic system, text is also control surface.&lt;/p&gt;

&lt;p&gt;This is why I still like a simpler &lt;a href="https://www.techiemike.com/self-hosted-llms-with-docker-ollama-and-open-webui/" rel="noopener noreferrer"&gt;self-hosted LLM setup with Docker, Ollama, and Open WebUI&lt;/a&gt; for a lot of personal work. A local chat interface that helps me think is a very different risk profile from a workflow agent with repository permissions and public posting rights.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Use narrower tokens and installation scopes
&lt;/h3&gt;

&lt;p&gt;Org-wide access is comfortable for setup and dangerous in production.&lt;/p&gt;

&lt;p&gt;Use repository-specific installation permissions where you can. Separate read access from write access. Separate code access from issue-management access. Separate internal automation from external-facing bots.&lt;/p&gt;

&lt;p&gt;The goal is not elegance. The goal is blast-radius reduction.&lt;/p&gt;

&lt;p&gt;When something goes wrong, you want one repo affected, not twenty.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Log the agent's tool calls like you would log an admin session
&lt;/h3&gt;

&lt;p&gt;If an agent fetches a private file because a public issue nudged it there, you want a trail.&lt;/p&gt;

&lt;p&gt;Log which trigger fired.&lt;br&gt;
Log what repositories were touched.&lt;br&gt;
Log what tools were called.&lt;br&gt;
Log what text was posted back out.&lt;/p&gt;

&lt;p&gt;Without that, your first sign of trouble is often the leaked output itself.&lt;/p&gt;

&lt;p&gt;I would also add one practical rule: run new workflows in dry-run mode against test repositories first. If a public comment can influence a private read in staging, it will do it in production too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitLost teaches CS students
&lt;/h2&gt;

&lt;p&gt;This is a strong teaching example because it bundles several core ideas into one incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication is not authorization
&lt;/h3&gt;

&lt;p&gt;The attacker did not need to break into the private repository directly. The system's own authorized agent did the reading on the attacker's behalf.&lt;/p&gt;

&lt;p&gt;That is an authorization design failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Public input can change private outcomes
&lt;/h3&gt;

&lt;p&gt;Students often think of user input as something that affects only the page or form it was typed into. Agent systems break that mental model. A sentence written in one place can trigger actions somewhere else entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security is a property of the whole system
&lt;/h3&gt;

&lt;p&gt;The model, the workflow trigger, the GitHub token, the repository permissions, and the response path all matter together. Focusing on only one piece misses the point.&lt;/p&gt;

&lt;p&gt;That is why I find incidents like this more useful than generic AI doom-posting. They are concrete. You can diagram the data flow. You can point to the trust boundary. You can explain exactly where the design went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  My bottom line on agent workflows
&lt;/h2&gt;

&lt;p&gt;I like agents. I use them. I think they are useful.&lt;/p&gt;

&lt;p&gt;I also think a lot of people are wiring them into production systems far too casually.&lt;/p&gt;

&lt;p&gt;GitLost is the warning shot. Not because AI is uniquely dangerous, but because it shows how quickly old access-control mistakes turn into leaks when you automate them.&lt;/p&gt;

&lt;p&gt;If your agent reads public text, touches private code, and posts publicly without a human checkpoint, fix that before you worry about the next model upgrade.&lt;/p&gt;

&lt;p&gt;That is the part worth taking seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://noma.security/blog/gitlost-how-we-tricked-githubs-ai-agent-into-leaking-private-repos/" rel="noopener noreferrer"&gt;Noma Labs: GitLost — How We Tricked GitHub's AI Agent into Leaking Private Repos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://news.ycombinator.com/item?id=48827858" rel="noopener noreferrer"&gt;Hacker News discussion of GitLost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techiemike.com/ollama-security-self-hosted-ai/" rel="noopener noreferrer"&gt;My post on self-hosted AI security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techiemike.com/multi-agent-debate-ai-architecture/" rel="noopener noreferrer"&gt;My post on multi-agent debate and AI architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Linux Signals Explained — SIGTERM, SIGKILL and Process Control</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Thu, 16 Jul 2026 02:15:01 +0000</pubDate>
      <link>https://dev.to/techiemike/linux-signals-explained-sigterm-sigkill-and-process-control-1nc0</link>
      <guid>https://dev.to/techiemike/linux-signals-explained-sigterm-sigkill-and-process-control-1nc0</guid>
      <description>&lt;h1&gt;
  
  
  Linux Signals Explained — SIGTERM, SIGKILL and Process Control
&lt;/h1&gt;

&lt;p&gt;Pressing &lt;code&gt;Ctrl+C&lt;/code&gt; looks simple until the day it stops working.&lt;/p&gt;

&lt;p&gt;You hit it in a terminal. One Python script exits immediately. A Docker container hangs for ten seconds before disappearing. A runaway process ignores you completely until &lt;code&gt;kill -9&lt;/code&gt; ends the argument.&lt;/p&gt;

&lt;p&gt;That difference is signals.&lt;/p&gt;

&lt;p&gt;I teach Cambridge Computer Science, and this is the gap the syllabus never bridges cleanly. Students learn hardware interrupts, interrupt priority levels, and the interrupt dispatch table. Then they sit down at a Linux terminal and meet &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;SIGTERM&lt;/code&gt;, and &lt;code&gt;SIGKILL&lt;/code&gt; with no explanation of how those ideas connect. In practice, signals are the missing link. They are how the kernel talks to processes.&lt;/p&gt;

&lt;p&gt;My view is blunt: signals should be taught right after interrupt handling, not left as Unix trivia for students to trip over later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ctrl+C is SIGINT, not magic
&lt;/h2&gt;

&lt;p&gt;When you press &lt;code&gt;Ctrl+C&lt;/code&gt;, the terminal sends &lt;code&gt;SIGINT&lt;/code&gt; to the foreground process group. The default action is termination, but a program can catch it and decide what to do next.&lt;/p&gt;

&lt;p&gt;That is why a shell script can react to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'echo "Caught SIGINT"; exit'&lt;/span&gt; INT
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"still running"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that, press &lt;code&gt;Ctrl+C&lt;/code&gt;, and the script does not just vanish. It receives a signal, runs the handler, prints the message, and exits cleanly.&lt;/p&gt;

&lt;p&gt;Python does the same thing in its own way. When you press &lt;code&gt;Ctrl+C&lt;/code&gt; during a Python program, Python translates &lt;code&gt;SIGINT&lt;/code&gt; into &lt;code&gt;KeyboardInterrupt&lt;/code&gt;. Students often think the traceback is the problem. It is actually the clue. The interpreter is telling you that the kernel interrupted the process and Python surfaced it in a readable form.&lt;/p&gt;

&lt;p&gt;This matters because it shows the first rule of signals: a signal is not always an instant kill switch. Sometimes it is a request. The process gets a chance to respond.&lt;/p&gt;

&lt;h2&gt;
  
  
  SIGTERM is the polite knock
&lt;/h2&gt;

&lt;p&gt;If you run &lt;code&gt;kill 1234&lt;/code&gt; with no extra option, Linux sends &lt;code&gt;SIGTERM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the normal shutdown signal. It tells the process to finish what it is doing and leave properly. A well-behaved process catches &lt;code&gt;SIGTERM&lt;/code&gt;, flushes buffers, closes files, saves state, and exits with dignity.&lt;/p&gt;

&lt;p&gt;This is the distinction I care about on my own Linux boxes. When I stop services on a Docker host, I do not want a process ripped out of memory halfway through a write. I want it to shut down cleanly.&lt;/p&gt;

&lt;p&gt;Here is the simplest Python example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received signal &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;signum&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Closing cleanly...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;working...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buffers flushed. Connections closed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also explains what Docker is doing. &lt;code&gt;docker stop&lt;/code&gt; does not begin with &lt;code&gt;SIGKILL&lt;/code&gt;. It sends &lt;code&gt;SIGTERM&lt;/code&gt; first, waits for the grace period, then escalates only if the process refuses to go.&lt;/p&gt;

&lt;p&gt;That is why a bad container shutdown usually looks like a ten-second pause. Docker is giving PID 1 inside the container time to behave.&lt;/p&gt;

&lt;p&gt;If you have read my post on &lt;a href="https://www.techiemike.com/learning-docker-by-building-a-container-engine-from-scratch/" rel="noopener noreferrer"&gt;building a container engine from scratch&lt;/a&gt;, this is the practical side of the same kernel mechanics. Containers are just processes with isolation wrapped around them. They still live and die by signals.&lt;/p&gt;

&lt;p&gt;The Redis example is a good one here. In my &lt;a href="https://www.techiemike.com/self-host-redis-8-8-with-docker-compose-and-why-its-useful-for-your-homelab-3/" rel="noopener noreferrer"&gt;Redis 8.8 homelab guide&lt;/a&gt;, the container is useful only if its shutdown is clean. Databases, caches, and queue workers need time to flush work to disk. If you know Docker sends &lt;code&gt;SIGTERM&lt;/code&gt; first and &lt;code&gt;SIGKILL&lt;/code&gt; later, &lt;code&gt;stop_grace_period&lt;/code&gt; stops looking like a random YAML setting and starts looking like what it really is: extra time for the process to do the right thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rzvqy0hdt011tklrsj8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rzvqy0hdt011tklrsj8.webp" alt="Docker graceful shutdown timeline showing a container receiving SIGTERM, a 10-second grace period, and SIGKILL only if the process does not exit in time" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SIGKILL is the last resort
&lt;/h2&gt;

&lt;p&gt;Signal 9 cannot be caught, blocked, or ignored. The kernel does not negotiate. It removes the process from the scheduler, tears down its resources, and that is the end of it.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;kill -9&lt;/code&gt; works when &lt;code&gt;Ctrl+C&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;You can prove the difference with a deliberately stubborn script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIG_IGN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it. Press &lt;code&gt;Ctrl+C&lt;/code&gt;. Nothing happens, because the process is ignoring &lt;code&gt;SIGINT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now find the PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Students remember this one because it feels dramatic, but the real lesson is not to reach for &lt;code&gt;kill -9&lt;/code&gt; as a habit. The real lesson is the opposite. Use it only when the polite options failed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SIGKILL&lt;/code&gt; gives the process no chance to flush, save, or clean up. If you fire it at a database, log writer, or container doing disk work, you are choosing force over correctness.&lt;/p&gt;

&lt;p&gt;It matters in container land too. Docker eventually escalates to &lt;code&gt;SIGKILL&lt;/code&gt; for the same reason you do: sometimes a process is stuck and has to go. The Linux OOM killer may terminate a process using &lt;code&gt;SIGKILL&lt;/code&gt;. A container killed with &lt;code&gt;SIGKILL&lt;/code&gt; commonly exits with code 137. Running out of memory is one possible cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signals students meet without realising it
&lt;/h2&gt;

&lt;p&gt;Here are the ones worth knowing first. If you want the canonical reference for default actions and edge cases, the Linux &lt;code&gt;signal(7)&lt;/code&gt; page at &lt;a href="https://man7.org/linux/man-pages/man7/signal.7.html" rel="noopener noreferrer"&gt;man7.org&lt;/a&gt; is the one worth bookmarking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;Default action&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SIGHUP&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Terminal closed, or daemon reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGINT&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+C&lt;/code&gt; from keyboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGKILL&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Forced kill by kernel or admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGSEGV&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Core dump&lt;/td&gt;
&lt;td&gt;Invalid memory access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGPIPE&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Wrote to a pipe with no reader&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGTERM&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Normal shutdown request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGCONT&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;Resume a stopped process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGSTOP&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;Forced stop that cannot be ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIGTSTP&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+Z&lt;/code&gt; from terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  SIGPIPE: why &lt;code&gt;yes | head -5&lt;/code&gt; stops
&lt;/h3&gt;

&lt;p&gt;This one is all over Linux even if nobody names it.&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;yes&lt;/code&gt; would happily print forever. &lt;code&gt;head&lt;/code&gt; reads five lines and closes its end of the pipe. When &lt;code&gt;yes&lt;/code&gt; tries to write again, the kernel sends &lt;code&gt;SIGPIPE&lt;/code&gt;. Default action: terminate.&lt;/p&gt;

&lt;p&gt;That is why the command ends cleanly instead of flooding your terminal forever.&lt;/p&gt;

&lt;p&gt;If you have already worked through my post on &lt;a href="https://www.techiemike.com/linux-pipes-redirection/" rel="noopener noreferrer"&gt;Linux pipes and redirection&lt;/a&gt;, this is the next piece of the picture. Pipes are not just text plumbing. They are kernel objects with rules, and one of those rules is that a writer with no readers gets told to stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  SIGHUP: why a remote job dies when SSH drops
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SIGHUP&lt;/code&gt; began as the hangup signal for a terminal line going away. The name is old, but the behaviour still matters.&lt;/p&gt;

&lt;p&gt;A student starts a long-running script over SSH, closes the laptop, comes back later, and the process is gone. That is usually not mysterious. The session closed, the shell's children got &lt;code&gt;SIGHUP&lt;/code&gt;, and the default action terminated them.&lt;/p&gt;

&lt;p&gt;The fix is not magic either. Use &lt;code&gt;nohup&lt;/code&gt;, &lt;code&gt;tmux&lt;/code&gt;, or &lt;code&gt;screen&lt;/code&gt; so the process is not tied to that one terminal session.&lt;/p&gt;

&lt;h3&gt;
  
  
  SIGSTOP and SIGCONT: pausing a process without killing it
&lt;/h3&gt;

&lt;p&gt;Start a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl+Z&lt;/code&gt; and the shell sends &lt;code&gt;SIGTSTP&lt;/code&gt;, which is the terminal stop signal. Then you can continue it with &lt;code&gt;fg&lt;/code&gt; or send &lt;code&gt;SIGCONT&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;You can also force the stop with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-STOP&lt;/span&gt; 
&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-CONT&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That maps nicely onto Cambridge process states. A stopped process is not finished. It is paused until the kernel allows it to continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Segmentation faults are signals too
&lt;/h2&gt;

&lt;p&gt;A segmentation fault is usually &lt;code&gt;SIGSEGV&lt;/code&gt;, signal 11. The process touched memory it was not allowed to touch, so the kernel terminated it. That is process protection doing exactly what it should do.&lt;/p&gt;

&lt;p&gt;This is one of the clearest places where operating system theory becomes real. The OS is enforcing memory boundaries between processes. Without that rule, one broken program could scribble over everything else in RAM.&lt;/p&gt;

&lt;p&gt;So when you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Segmentation fault (core dumped)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that is not just an error message. It is the kernel reporting a memory protection failure and sending the signal that ends the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cambridge connection: signals and interrupt handling
&lt;/h2&gt;

&lt;p&gt;This is the part students should be taught explicitly.&lt;/p&gt;

&lt;p&gt;In Chapter 16, Cambridge students learn the interrupt model: a device raises an interrupt, the CPU saves state, the interrupt dispatch table is consulted, a service routine runs, then the previous state is restored and execution continues.&lt;/p&gt;

&lt;p&gt;Signal delivery follows the same pattern at process level.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An event occurs: keyboard interrupt, parent command, broken pipe, invalid memory access.&lt;/li&gt;
&lt;li&gt;The kernel marks a signal as pending for the process or thread.&lt;/li&gt;
&lt;li&gt;When control is returning to user mode, the kernel checks whether that signal is blocked, ignored, or caught.&lt;/li&gt;
&lt;li&gt;If a handler exists, the kernel arranges for that handler to run.&lt;/li&gt;
&lt;li&gt;After the handler finishes, &lt;code&gt;sigreturn&lt;/code&gt; restores the saved state.&lt;/li&gt;
&lt;li&gt;The process continues, exits, stops, or dumps core depending on the signal and disposition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a loose analogy. It maps neatly onto the same interrupt pattern students already know, just at process level rather than hardware level.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.techiemike.com/youre-the-os-teaching-process-memory-and-i-o-management-through-gaming/" rel="noopener noreferrer"&gt;You're the OS&lt;/a&gt;, I teach interrupt handling as a game because students understand it faster when they have to make the decisions themselves. Signals fit that same lesson perfectly. The kernel is still the traffic controller. The only difference is that now the conversation is between the kernel and a process rather than the CPU and a hardware device.&lt;/p&gt;

&lt;p&gt;You can even inspect part of this on a live Linux system in &lt;code&gt;/proc//status&lt;/code&gt;. Try &lt;code&gt;grep -E "Sig(Pnd|Blk|Ign|Cgt)" /proc/1/status&lt;/code&gt; and you will see the signal bitmaps immediately. Fields such as &lt;code&gt;SigPnd&lt;/code&gt;, &lt;code&gt;SigBlk&lt;/code&gt;, &lt;code&gt;SigIgn&lt;/code&gt;, and &lt;code&gt;SigCgt&lt;/code&gt; show which signals are pending, blocked, ignored, or caught. That is not abstract theory. That is the kernel's process-control dashboard sitting in plain text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0iczwcb06gjyxbzaacae.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0iczwcb06gjyxbzaacae.webp" alt="Kernel-to-process signal flow diagram showing event generation, pending signal check, handler execution, and sigreturn restoring process state — the software mirror of interrupt handling" width="800" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What students should remember
&lt;/h2&gt;

&lt;p&gt;If you remember only four things, remember these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Ctrl+C&lt;/code&gt; sends &lt;code&gt;SIGINT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kill&lt;/code&gt; sends &lt;code&gt;SIGTERM&lt;/code&gt; by default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kill -9&lt;/code&gt; sends &lt;code&gt;SIGKILL&lt;/code&gt;, and the process gets no say in the matter.&lt;/li&gt;
&lt;li&gt;A lot of weird Linux behaviour is not weird at all once you know which signal fired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That includes broken SSH jobs, stuck containers, dead pipelines, and segfaults.&lt;/p&gt;

&lt;p&gt;Signals are not obscure Unix trivia. They are core process control. They explain how the kernel interrupts, stops, resumes, warns, and kills running programs. For CS students, they are the cleanest bridge between textbook interrupt handling and the real behaviour of a Linux system.&lt;/p&gt;

&lt;p&gt;The next time a process ignores &lt;code&gt;Ctrl+C&lt;/code&gt;, ask a better question: which signal did it get, and what is it allowed to do with it?&lt;/p&gt;

</description>
      <category>linux</category>
      <category>tutorial</category>
      <category>processes</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Proxmox VE in Production — What Running It 24/7 for Over a Year Actually Teaches You</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Wed, 15 Jul 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/techiemike/proxmox-ve-in-production-what-running-it-247-for-over-a-year-actually-teaches-you-iil</link>
      <guid>https://dev.to/techiemike/proxmox-ve-in-production-what-running-it-247-for-over-a-year-actually-teaches-you-iil</guid>
      <description>&lt;h1&gt;
  
  
  Proxmox VE in Production — What Running It 24/7 for Over a Year Actually Teaches You
&lt;/h1&gt;

&lt;p&gt;The moment an outage interrupts something you actually use every day, your homelab has stopped behaving like a toy.&lt;/p&gt;

&lt;p&gt;That is the cleanest definition of production I know.&lt;/p&gt;

&lt;p&gt;I have been running Proxmox VE 24/7 for over a year on my &lt;a href="https://www.techiemike.com/chuwi-minibook-x-vs-bmax-pro-8-which-budget-homelab-machine/" rel="noopener noreferrer"&gt;BMAX Pro 8&lt;/a&gt;. On paper, it is still a homelab box. In practice, Plex, Nextcloud, and Home Assistant all depend on it staying healthy. That changes the standard immediately. You stop caring about how quickly you can spin up a VM and start caring about how calmly you can recover when something breaks.&lt;/p&gt;

&lt;p&gt;Most Proxmox posts online are about installation, upgrade paths, or first impressions. That is useful, and I have already covered the build side in my &lt;a href="https://www.techiemike.com/proxmox-homelab-setup-on-a-mini-pc-ubuntu-vms-beyond/" rel="noopener noreferrer"&gt;Proxmox homelab setup on a mini PC guide&lt;/a&gt;. What those posts usually skip is the boring middle: the long stable stretches, the one migration that runs far too long, the backup routine you should have tightened earlier, and the network blip that changes how you think about remote access.&lt;/p&gt;

&lt;p&gt;After 400+ days, my opinion is simple: Proxmox is excellent, but it rewards boring operators and punishes optimistic ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production in a homelab still means consequences
&lt;/h2&gt;

&lt;p&gt;People get strangely hung up on the word production.&lt;/p&gt;

&lt;p&gt;If you are not billing customers, they assume it does not count. I think that misses the point. If a service is part of your normal day, if downtime immediately becomes your problem, and if recovery matters more than screenshots, you are already there.&lt;/p&gt;

&lt;p&gt;A home setup does not need a sales team behind it to deserve production habits. It only needs real consequences.&lt;/p&gt;

&lt;p&gt;When Plex buffers, someone notices. When Nextcloud disappears, the missing files matter. When Home Assistant falls over, the problem stops being an interesting virtualization issue and starts being a broken part of the day. Those are the same kinds of day-to-day services I wrote about in my &lt;a href="https://www.techiemike.com/self-hosting-toolkit-10-apps/" rel="noopener noreferrer"&gt;self-hosting toolkit&lt;/a&gt;, and they are exactly why I no longer treat uptime as a hobby metric.&lt;/p&gt;

&lt;p&gt;That is why I no longer judge infrastructure by how clever it looks. I judge it by how easy it is to understand when I am tired. Fancy is cheap. Recoverable is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: Storage migrations are where confidence goes to die
&lt;/h2&gt;

&lt;p&gt;A fresh Proxmox install makes storage look neat and tidy. Local disks here. VM disks there. Buttons in the web interface. Progress bars. It all feels manageable.&lt;/p&gt;

&lt;p&gt;Then you do a real migration on a live box.&lt;/p&gt;

&lt;p&gt;One of the clearest lessons from this past year came from moving a VM disk between local datastores on the mini PC. On paper it was a routine reshuffle. In practice it ran for hours, and while it ran, the whole box crawled. Plex playback stuttered. Nextcloud got sluggish. The migration was saturating the machine's own disk IO, and every other service was standing in the same queue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8bcxtgfchjdqng1e682.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8bcxtgfchjdqng1e682.webp" alt="Storage migration visualization showing VM disk transfer between datastores" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the part people underestimate on a single-node homelab. A storage move is not a copy that happens quietly in the background — it is an IO-bound operation that competes directly with everything your services need. If the guest is busy dirtying memory while it moves, the tail end drags even longer. And if your Proxmox system disk is something cheap and slow, that one weakness drags the entire host down with it. A storage migration is not housekeeping once the system is busy. It is an operational event, and it belongs in a maintenance window.&lt;/p&gt;

&lt;p&gt;This is where new homelab thinking usually goes wrong. You treat storage moves like rearranging folders on a laptop. Proxmox treats them like large, stateful operations touching the thing your services are standing on.&lt;/p&gt;

&lt;p&gt;The teacher version is simple: moving a VM disk sounds like changing seats in a classroom. It is not. It is more like moving the entire classroom while the lesson is still running.&lt;/p&gt;

&lt;p&gt;What changed for me after that long move?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I stopped doing storage work casually.&lt;/li&gt;
&lt;li&gt;I started assuming every simple move would take longer than I wanted.&lt;/li&gt;
&lt;li&gt;I became much more suspicious of doing anything big during the hours when the services were actually in use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds obvious. It is obvious. The problem is that you only really believe it after one migration ruins your evening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: Backups do not mature by themselves
&lt;/h2&gt;

&lt;p&gt;For a while, backups in a homelab often exist as a good intention.&lt;/p&gt;

&lt;p&gt;You know they matter. You mean to tighten them up. You tell yourself the current setup is fine for now.&lt;/p&gt;

&lt;p&gt;It usually is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4025krajeb20ybb9ahgv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4025krajeb20ybb9ahgv.webp" alt="Automated Proxmox Backup Server with retention policies" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My backup strategy evolved from manual &lt;code&gt;vzdump&lt;/code&gt; jobs to automated Proxmox Backup Server with retention policies. The real improvement was not the acronym. It was moving from "I should make a backup tonight" to scheduled, deduplicated backups that ran whether I was organised or not.&lt;/p&gt;

&lt;p&gt;Manual backups feel responsible because they produce files. Manual anything has the same weakness: it depends on you being awake, consistent, and not distracted by ten other jobs. That works for a week. It does not work for a year.&lt;/p&gt;

&lt;p&gt;What PBS changed was the shape of the problem. Instead of wondering whether I had remembered to capture a fresh state, I had a backup system with a schedule, retention, and an actual operational rhythm behind it. The policy is boring on purpose: a daily job at 2 AM, retention set to keep seven daily, four weekly, and six monthly snapshots, so I always have a recent restore point without the datastore growing forever. If you want the vendor view, the &lt;a href="https://pbs.proxmox.com/docs/" rel="noopener noreferrer"&gt;Proxmox Backup Server documentation&lt;/a&gt; is worth bookmarking because it makes the retention and datastore model much clearer than trial and error ever will.&lt;/p&gt;

&lt;p&gt;One habit matters more than the schedule itself: a weekly verify job, so PBS actually checks the backups are readable rather than just present. I will be honest about the one I do not keep — I have never sat down and tested a full restore. I have never needed to, which is either luck or a quiet risk I keep meaning to close, because a backup you have never restored is a hope, not a backup. One rule I do follow without exception: the backups do not live on the same physical disk as the VMs. If that disk dies, I do not want it taking the backups with it.&lt;/p&gt;

&lt;p&gt;Once the box started carrying important services, I stopped thinking about backups as insurance and started thinking about them as part of the service itself. If the restore path is weak, the service is weak. That is it.&lt;/p&gt;

&lt;p&gt;This is also where Proxmox teaches the same lesson every good sysadmin eventually learns: boring automation beats heroic memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: The network is the part you forget until it bites
&lt;/h2&gt;

&lt;p&gt;Every long-running setup has one moment that changes how you think about it.&lt;/p&gt;

&lt;p&gt;For me it was a brief network blip that, for a few minutes, cut me off from the box entirely. Nothing was on fire. The services kept running locally. But the reverse proxy in front of everything, the tunnel I use to reach it from outside the house, and my remote management were all suddenly unreachable at once. On a single-node homelab, that is the quiet risk nobody warns you about: the network path to your box is a single point, and when it wobbles, you lose the ability to see or fix anything precisely when you most want to.&lt;/p&gt;

&lt;p&gt;The network is not a side topic. It is the topic.&lt;/p&gt;

&lt;p&gt;I teach computer science, and this is the bit textbooks often flatten too much. On paper, a network is a tidy diagram with arrows. In practice, it is the one dependency sitting underneath every other dependency. When it drops, the dashboard that would tell you what is wrong is on the far side of the thing that is wrong.&lt;/p&gt;

&lt;p&gt;That blip made me much less casual about the phrase "just a brief network issue." Brief for whom? The link that recovers on its own in thirty seconds, or you, standing in a coffee shop unable to reach a single service until it does? It also made me plan for it: a way in that does not depend on the one path that just failed, and notes stored somewhere other than the machine I am trying to reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rules I would write down on day one
&lt;/h2&gt;

&lt;p&gt;This is the section I wish more Proxmox posts included.&lt;/p&gt;

&lt;p&gt;Not philosophy. Operating rules.&lt;/p&gt;

&lt;p&gt;If I were setting up the same box again with the same requirement that these services need to stay dependable, I would write down this checklist on day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real maintenance window for any storage move, not "whenever I get time tonight."&lt;/li&gt;
&lt;li&gt;A PBS backup job on a schedule with a written retention policy — mine keeps seven daily, four weekly, and six monthly.&lt;/li&gt;
&lt;li&gt;Backups on a different physical disk from the VMs, so one dead drive cannot take both.&lt;/li&gt;
&lt;li&gt;A restore-test habit — the one I still owe myself — because successful backups do not prove successful recovery.&lt;/li&gt;
&lt;li&gt;Alerts that reach me before I stumble into a problem: disk usage crossing 80%, failed backup jobs, and offline services.&lt;/li&gt;
&lt;li&gt;A second way to reach the box that does not depend on the one network path that just failed.&lt;/li&gt;
&lt;li&gt;Clear notes stored outside the Proxmox host, so recovery steps do not disappear with the machine you are trying to recover.&lt;/li&gt;
&lt;li&gt;A power plan: UPS and a graceful shutdown, or at least a decision made on purpose instead of by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is glamorous. That is exactly why it matters.&lt;/p&gt;

&lt;p&gt;The romantic version of self-hosting is building the thing. The grown-up version is keeping it boring six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 400 days actually teaches you
&lt;/h2&gt;

&lt;p&gt;Running Proxmox for a weekend tells you whether the install worked.&lt;/p&gt;

&lt;p&gt;Running it 24/7 for over a year tells you whether your habits work.&lt;/p&gt;

&lt;p&gt;Proxmox itself has been solid. The harder lessons came from operations: storage work takes longer than you think, backup maturity does not happen by accident, the network path to your box deserves more respect than the dashboard encourages, and small shortcuts become large problems once the box matters.&lt;/p&gt;

&lt;p&gt;So yes, you can run Proxmox VE in production.&lt;/p&gt;

&lt;p&gt;The better question is whether you are ready to run it like boring, dependable infrastructure. That is the standard that matters.&lt;/p&gt;

</description>
      <category>proxmox</category>
      <category>linux</category>
      <category>devops</category>
      <category>homelab</category>
    </item>
    <item>
      <title>The CISA GitHub Leak: What It Should Teach Homelab Owners</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Tue, 14 Jul 2026 02:00:01 +0000</pubDate>
      <link>https://dev.to/techiemike/the-cisa-github-leak-what-it-should-teach-homelab-owners-2p1l</link>
      <guid>https://dev.to/techiemike/the-cisa-github-leak-what-it-should-teach-homelab-owners-2p1l</guid>
      <description>&lt;h1&gt;
  
  
  The CISA GitHub Leak: Homelab Security Lessons for Self-Hosters
&lt;/h1&gt;

&lt;p&gt;In May 2026, researchers discovered a public GitHub repository maintained by a CISA contractor containing credentials, deployment material, internal documentation, and cloud infrastructure details, as detailed in &lt;a href="https://krebsonsecurity.com/2026/05/cisa-admin-leaked-aws-govcloud-keys-on-github/" rel="noopener noreferrer"&gt;Krebs on Security's report on the leak&lt;/a&gt;. CISA removed the repository and began investigating the exposure. The repository reportedly contained plaintext passwords, AWS tokens, certificates, infrastructure code, deployment files and internal documentation. Some credentials were reportedly still valid when discovered.&lt;/p&gt;

&lt;p&gt;That is not the same thing as some dramatic headline about CISA getting completely owned. It is a repository leak, and that distinction matters. It also happens to be the part self-hosters should care about most, because this is exactly the kind of mistake that turns an ordinary lab into an easy target.&lt;/p&gt;

&lt;p&gt;I think this is the more useful kind of security story anyway. A lot of homelab failures do not begin with a clever exploit. They begin with a secret in the wrong place, a service exposed longer than intended, or a trust boundary that only existed in your head.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep secrets out of repositories
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofbe4ybxw258t9vw6i3b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofbe4ybxw258t9vw6i3b.webp" alt="Illustration of exposed credentials and commit history showing how secrets leaked from a GitHub repository can compromise a homelab" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most direct lesson from the CISA incident is not about firewalls or VLANs. It is about credential handling.&lt;/p&gt;

&lt;p&gt;Never commit passwords, API keys, private keys, access tokens, .env files, database exports, or backup archives to a repository. A repository named "private" is not a security control, and deleting a secret from the latest commit does not remove it from Git history.&lt;/p&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep secrets in environment files excluded by .gitignore&lt;/li&gt;
&lt;li&gt;use a password manager or secrets-management system&lt;/li&gt;
&lt;li&gt;enable repository secret scanning and push protection&lt;/li&gt;
&lt;li&gt;rotate a credential immediately if it has been committed&lt;/li&gt;
&lt;li&gt;inspect Git history, not only the current working tree&lt;/li&gt;
&lt;li&gt;give tokens only the permissions and lifetime they require&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A leaked credential should be treated as compromised even if there is no evidence that somebody used it.&lt;/p&gt;

&lt;p&gt;That last point is where people still talk themselves into delay. They tell themselves nothing bad happened, so there is nothing to rotate. I do not buy that. If a token was in a public repo, the safe assumption is that it is gone.&lt;/p&gt;

&lt;p&gt;The same principle showed up in my write-up on &lt;a href="https://www.techiemike.com/1-click-github-token-theft-via-vscode-bug-a-developer-security-wake-up-call/" rel="noopener noreferrer"&gt;GitHub token theft through a VS Code bug&lt;/a&gt;. The bug mattered, but the real damage came from what a leaked credential could unlock after it escaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit what is actually exposed
&lt;/h2&gt;

&lt;p&gt;A surprising number of homelabs run on assumption. The owner thinks only 80 and 443 are open because that is what they meant to expose six months ago.&lt;/p&gt;

&lt;p&gt;Check it from the outside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-Pn&lt;/span&gt; yourdomain.com
&lt;span class="nb"&gt;sudo &lt;/span&gt;nmap &lt;span class="nt"&gt;-Pn&lt;/span&gt; &lt;span class="nt"&gt;-p-&lt;/span&gt; your-public-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command checks common TCP ports. The second scans all TCP ports and takes longer. UDP requires a separate scan.&lt;/p&gt;

&lt;p&gt;Run the external scan against your known public IP from a device outside the home network. Do not rely on scanning a proxied hostname, because that may test Cloudflare's edge rather than your router or server.&lt;/p&gt;

&lt;p&gt;Also, scan only systems you own or have permission to test. That should be obvious, but it still needs saying.&lt;/p&gt;

&lt;p&gt;Then check the host itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tulpn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you the local listening sockets. The question is not just what is running. The question is what is reachable from somewhere it should not be reachable from.&lt;/p&gt;

&lt;p&gt;If you self-host AI services, this matters even more because too many people publish them first and think about access control later. A service with weak or missing auth becomes a very different problem the moment it stops being localhost-only.&lt;/p&gt;

&lt;p&gt;One blunt rule helps here: if you no longer remember why a port is open, close it until you do remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put authentication in front of selected services
&lt;/h2&gt;

&lt;p&gt;Weak authentication still causes more real damage than most fancy attack chains.&lt;/p&gt;

&lt;p&gt;If a service supports MFA, turn it on. If it does not, put something in front of it that does. Authelia, Authentik, and &lt;a href="https://www.techiemike.com/cloudflare-self-managed-oauth-homelab-security/" rel="noopener noreferrer"&gt;Cloudflare Access&lt;/a&gt; are all reasonable ways to stop a login page from sitting naked on the internet.&lt;/p&gt;

&lt;p&gt;This only works when users cannot bypass the authentication proxy and connect directly to the backend service. Restrict the origin with firewall rules, private networking, or binding to localhost where appropriate.&lt;/p&gt;

&lt;p&gt;MFA and proxy authentication reduce the risk of stolen login passwords, but they do not necessarily protect API tokens, session cookies, application keys, or credentials that bypass the interactive login.&lt;/p&gt;

&lt;p&gt;That is why I like layered controls more than single-product confidence. A reverse proxy is useful. A reverse proxy plus localhost binding plus tight firewall rules is better.&lt;/p&gt;

&lt;p&gt;The point is the same either way: do not confuse a clean login page with real isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Segment the network properly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgory46itl1qkrv7n2g4t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgory46itl1qkrv7n2g4t.webp" alt="Network segmentation diagram showing separate VLANs for main devices, homelab servers, IoT devices, and guest Wi-Fi in a homelab" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flat networks are lazy, and they are generous to attackers.&lt;/p&gt;

&lt;p&gt;If your laptop, phone, TV, random IoT plug, and Proxmox host all sit in one easy broadcast neighbourhood, one compromise can turn into a walk across your whole lab.&lt;/p&gt;

&lt;p&gt;A simple layout is already better than the usual everything-on-one-LAN mess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main devices: 192.168.10.0/24&lt;/li&gt;
&lt;li&gt;Homelab servers: 192.168.20.0/24&lt;/li&gt;
&lt;li&gt;IoT devices: 192.168.30.0/24&lt;/li&gt;
&lt;li&gt;Guest Wi-Fi: 192.168.40.0/24&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then make the policy boring and strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guest reaches the internet, not your servers&lt;/li&gt;
&lt;li&gt;IoT reaches only what it actually needs&lt;/li&gt;
&lt;li&gt;Admin access to Proxmox, SSH, and dashboards comes from your trusted network only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different IP ranges alone do not provide segmentation. The network equipment must place them in separate VLANs or interfaces and enforce firewall rules between them.&lt;/p&gt;

&lt;p&gt;That is the part people skip. Changing the third octet is not segmentation. Enforcing traffic boundaries is segmentation.&lt;/p&gt;

&lt;p&gt;A compact setup is easier to secure because you can still explain it to yourself six months later. My &lt;a href="https://www.techiemike.com/proxmox-homelab-setup-on-a-mini-pc-ubuntu-vms-beyond/" rel="noopener noreferrer"&gt;Proxmox homelab setup on a mini PC&lt;/a&gt; and the &lt;a href="https://www.techiemike.com/chuwi-minibook-x-vs-bmax-pro-8-which-budget-homelab-machine/" rel="noopener noreferrer"&gt;BMAX Pro 8 budget homelab comparison&lt;/a&gt; both come back to the same point: extra complexity creates blind spots fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add brute-force protection and rate limits
&lt;/h2&gt;

&lt;p&gt;Anything with a login page gets hammered eventually.&lt;/p&gt;

&lt;p&gt;If you expose SSH, a reverse proxy, a dashboard, or any admin panel, assume bots are already trying it. Fail2ban still earns its place here, but only if it is actually watching the right logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy jail might produce output similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status for the jail: sshd
|- Filter
|  |- Currently failed: 3
|  `- Total failed:     19
`- Actions
   |- Currently banned: 1
   `- Total banned:     4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail2ban must be connected to the correct application or proxy logs. When traffic passes through Cloudflare or another proxy, configure trusted proxy handling carefully so the system reads the real client address without trusting spoofed headers.&lt;/p&gt;

&lt;p&gt;Pair that with firewall rules that make sense for the host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status numbered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are doing this over SSH, stage the allow rules first so you do not lock yourself out.&lt;/p&gt;

&lt;p&gt;Be aware that Docker manages its own firewall rules. A container port published on 0.0.0.0 may not behave as expected under a simple UFW policy. Check published ports with docker ps and test reachability externally.&lt;/p&gt;

&lt;p&gt;Reverse-proxy rate limits help too. They will not make a weak stack strong, but they do remove easy wins from bots that rely on unlimited retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch deliberately and on schedule
&lt;/h2&gt;

&lt;p&gt;Most self-hosters do not ignore updates because they are careless. They ignore updates because the process is vague, annoying, and easy to postpone.&lt;/p&gt;

&lt;p&gt;That is why cadence matters. Pick a rhythm and make it boring.&lt;/p&gt;

&lt;p&gt;For Docker-based services, I still prefer two separate habits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;notice that an image changed&lt;/li&gt;
&lt;li&gt;decide when to roll it out&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is why I like tools such as Diun or Dockcheck. They tell you something moved without automatically changing production. Auto-update tools have their place, but I would be careful with anything that touches your reverse proxy, auth layer, or database while you are asleep.&lt;/p&gt;

&lt;p&gt;A safer update cycle looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it when you can watch the result.&lt;/p&gt;

&lt;p&gt;If you harden containers, test those changes one at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;read_only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no-new-privileges:true&lt;/span&gt;
&lt;span class="na"&gt;cap_drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test these settings one at a time. A read-only container may still have writable volumes, and dropping every capability may break software that requires a specific capability. Add back only what the application demonstrably needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start where the risk drops fastest
&lt;/h2&gt;

&lt;p&gt;Do not turn this into a heroic weekend project.&lt;/p&gt;

&lt;p&gt;Start with the fix that removes the most risk in your own setup. Sometimes that is rotating an exposed token. Sometimes it is closing a port you forgot about. Sometimes it is putting one fragile service behind a proper auth layer and binding the backend to localhost.&lt;/p&gt;

&lt;p&gt;The CISA contractor leak is a useful warning because it is ordinary. There was no magic to it. Just credentials in the wrong place, followed by the usual cleanup.&lt;/p&gt;

&lt;p&gt;That is exactly why homelab owners should pay attention.&lt;/p&gt;

</description>
      <category>security</category>
      <category>linux</category>
      <category>devops</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Homelab Burnout: Sustainable Self-Hosting When the Hobby Starts Feeling Like Work</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Mon, 13 Jul 2026 02:00:01 +0000</pubDate>
      <link>https://dev.to/techiemike/homelab-burnout-sustainable-self-hosting-when-the-hobby-starts-feeling-like-work-3nh7</link>
      <guid>https://dev.to/techiemike/homelab-burnout-sustainable-self-hosting-when-the-hobby-starts-feeling-like-work-3nh7</guid>
      <description>&lt;h1&gt;
  
  
  Homelab Burnout: Sustainable Self-Hosting When the Hobby Starts Feeling Like Work
&lt;/h1&gt;

&lt;p&gt;Browse &lt;a href="https://www.reddit.com/r/homelab/" rel="noopener noreferrer"&gt;r/homelab&lt;/a&gt; for long enough and you will regularly find people questioning whether their homelab has become more work than fun. People are not tired of technology itself. They are tired of turning a useful hobby into an unpaid support contract.&lt;/p&gt;

&lt;p&gt;I teach computer science full-time, run a stack of self-hosted services, write here, and record YouTube videos. My homelab is useful, but only when it stays in its lane. The moment it starts demanding daily babysitting, it stops being a hobby and starts behaving like a badly managed side business.&lt;/p&gt;

&lt;p&gt;That is my opinion on this, and I will argue it plainly: a homelab should solve problems you actually have. The second it exists mainly to generate maintenance work, the learning value starts to disappear. You are just doing chores in Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  The homelab burnout escalation trap is real
&lt;/h2&gt;

&lt;p&gt;Most homelabs do not become messy in one big leap. They grow one perfectly reasonable decision at a time.&lt;/p&gt;

&lt;p&gt;You add Plex because you want your media in one place. Then you add Uptime Kuma because monitoring feels responsible. Then a reverse proxy, then a dashboard, then a note-taking tool, then an automation tool, then a second database because one app prefers Postgres and another wants Redis. A few weeks later you have twenty moving parts and half of them are there because they looked useful on somebody else's YouTube channel.&lt;/p&gt;

&lt;p&gt;That is how hobby scope creep works. Nobody wakes up and decides to build an unreliable mini data centre in the spare room. You just keep saying yes to one more service.&lt;/p&gt;

&lt;p&gt;I already trimmed my own stack once. In &lt;a href="https://www.techiemike.com/self-hosting-toolkit-10-apps/" rel="noopener noreferrer"&gt;Self-Hosting Toolkit: 10 Apps I Actually Use Every Day&lt;/a&gt;, the key phrase is "actually use." That was not branding. It was a survival rule. If a service does not save time, make money, teach me something worth keeping, or solve a regular problem in my week, it does not deserve a permanent slot.&lt;/p&gt;

&lt;p&gt;A lot of burnout comes from running software with no job description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation should remove toil, not create clever new failure modes
&lt;/h2&gt;

&lt;p&gt;Automation is the part everyone talks about, but many people automate the wrong things. They build a complicated chain of scripts, webhooks, and notifications, then spend Saturday debugging the automation instead of the original problem.&lt;/p&gt;

&lt;p&gt;Good homelab automation is boring. It cuts repeat work. It makes failure obvious. It does not need a diagram just to explain why your container restarted.&lt;/p&gt;

&lt;p&gt;A simple Docker health check earns its place because it answers a basic question: is this service alive or not?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app:1.4.2&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This assumes the image includes curl and the application exposes a /health endpoint. Otherwise, use a command already available inside the container or add an appropriate health-check method.&lt;/p&gt;

&lt;p&gt;That is useful. It replaces guesswork with one small signal. Pinning a known version also makes updates more deliberate and rollbacks easier.&lt;/p&gt;

&lt;p&gt;The same applies to update routines. I do not want to remember which container needs a manual pull, which one changed its environment variables, and which one quietly stopped two weeks ago. If you are relying on memory for routine maintenance, you already have a fragile system.&lt;/p&gt;

&lt;p&gt;This is also why I still like small, direct tooling. A maintenance script that checks container status, disk usage, and backup age is worth more than a beautiful dashboard I only open after something breaks. Monitoring should tell you what needs attention. It should not become another thing you maintain for its own sake. My &lt;a href="https://www.techiemike.com/hermes-agent-mini-pc-setup/" rel="noopener noreferrer"&gt;Hermes Agent mini PC setup guide&lt;/a&gt; comes from exactly that mindset: use automation to remove repeated admin work, not to build a second system you have to babysit.&lt;/p&gt;

&lt;p&gt;If you want the short version, build the minimum system that answers three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the service up?&lt;/li&gt;
&lt;li&gt;Is the data backed up?&lt;/li&gt;
&lt;li&gt;Will I notice when something fails?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything beyond that needs to justify itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries matter more than enthusiasm
&lt;/h2&gt;

&lt;p&gt;This is the part many homelab guides skip because it is less fun than listing tools.&lt;/p&gt;

&lt;p&gt;You need boundaries.&lt;/p&gt;

&lt;p&gt;Not every service should be self-hosted. Some are worth the effort because they protect privacy, centralise your workflow, or save ongoing subscription costs. Others are cheap in dollars and expensive in attention.&lt;/p&gt;

&lt;p&gt;The best question I know is this: what happens if this service breaks for 48 hours?&lt;/p&gt;

&lt;p&gt;If the honest answer is "nothing important," it probably should not sit in the centre of your setup. It might still be a good weekend experiment. It just should not become permanent infrastructure.&lt;/p&gt;

&lt;p&gt;That is the difference between a lab and a dependency. Labs are where you test ideas. Dependencies are the things that quietly ruin your week when they go down.&lt;/p&gt;

&lt;p&gt;For me, the keepers are the services that support real work: file storage, monitoring, automation, remote access, and a few tools I touch constantly. A smaller stack is not less serious. It is usually better engineered because you can still understand it on a tired Tuesday night. The same bias toward manageable infrastructure is why I built my &lt;a href="https://www.techiemike.com/proxmox-homelab-setup-on-a-mini-pc-ubuntu-vms-beyond/" rel="noopener noreferrer"&gt;Proxmox homelab setup on a mini PC&lt;/a&gt; around hardware I can understand, recover, and afford to leave running.&lt;/p&gt;

&lt;p&gt;A lot of people also underestimate security overhead here. Every exposed dashboard, admin panel, and reverse proxy route increases the attack surface. Limiting permanent services is therefore not only a maintenance decision; it is also a security discipline. That is why I treat access control and certificates as infrastructure, not decoration. If you use Cloudflare and want an additional authentication layer in front of selected services, &lt;a href="https://www.techiemike.com/cloudflare-self-managed-oauth-homelab-security/" rel="noopener noreferrer"&gt;Cloudflare Self-Managed OAuth&lt;/a&gt; is worth understanding. It complements rather than replaces secure configuration, updates, certificates, and network controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation is what saves you at 2 AM
&lt;/h2&gt;

&lt;p&gt;The most underrated homelab tool is not Docker, Proxmox, or a reverse proxy. It is documentation.&lt;/p&gt;

&lt;p&gt;I mean boring documentation. The kind that tells you which compose file matters, which ports are already in use, which volume paths hold real data, which containers depend on each other, and what to restore first after a bad update.&lt;/p&gt;

&lt;p&gt;Without that, every outage becomes archaeology.&lt;/p&gt;

&lt;p&gt;This is where my Obsidian vault earns its keep. I keep service notes, recovery steps, container locations, backup paths, and configuration decisions in one place. Not because it is elegant, but because I do not trust the tired version of myself to remember why something was set up a certain way six months ago.&lt;/p&gt;

&lt;p&gt;A useful service note can be brutally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service: Uptime Kuma
Compose file: /opt/monitoring/docker-compose.yml
Data path: /opt/docker/uptime-kuma
Depends on: reverse proxy + local DNS
Backup: nightly tar to external drive
Restore order: DNS -&amp;gt; proxy -&amp;gt; Kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That note has saved more time than any fancy dashboard ever has.&lt;/p&gt;

&lt;p&gt;The same habit applies to anyone learning to build real systems. Sustainable engineering is not just about writing code that works today. It is about leaving enough context behind that the next person, or future you, can support it without panic. Real systems fail. Good documentation turns failure from chaos into a checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics are not just about money
&lt;/h2&gt;

&lt;p&gt;Self-hosters love talking about savings, and sometimes the savings are real. A small mini PC, a couple of drives, and a handful of containers can absolutely replace several monthly subscriptions.&lt;/p&gt;

&lt;p&gt;But cost is not just hardware plus electricity.&lt;/p&gt;

&lt;p&gt;Time is part of the bill.&lt;/p&gt;

&lt;p&gt;If a service saves you $4 a month but costs two hours of maintenance every other weekend, that is not frugal. That is expensive in a different currency. The maths gets worse if the service is something you barely use.&lt;/p&gt;

&lt;p&gt;This is why I think the healthiest homelab mindset is selective self-hosting. Keep the things that give you one of these returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;control over important data&lt;/li&gt;
&lt;li&gt;a workflow improvement you genuinely notice&lt;/li&gt;
&lt;li&gt;a solid learning payoff&lt;/li&gt;
&lt;li&gt;a clear cost saving without constant babysitting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it gives you none of those, it is probably clutter.&lt;/p&gt;

&lt;p&gt;I also think there is a phase many homelabbers need to go through once: the purge. Not a dramatic teardown, just an honest audit. Open the compose files. List the services. Ask what each one is doing for you now, not what you hoped it would do when you installed it.&lt;/p&gt;

&lt;p&gt;Delete the tourists: services that arrived as experiments, contributed nothing, and never left.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick homelab sustainability audit
&lt;/h2&gt;

&lt;p&gt;For each permanent service, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did I use it during the last 30 days?&lt;/li&gt;
&lt;li&gt;Does it hold data that matters?&lt;/li&gt;
&lt;li&gt;Is it backed up and tested?&lt;/li&gt;
&lt;li&gt;Do I know how to restore it?&lt;/li&gt;
&lt;li&gt;Would replacing it with a hosted service reduce meaningful work?&lt;/li&gt;
&lt;li&gt;Is its learning value still greater than its maintenance cost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A service that fails most of those questions probably belongs in the lab, not in permanent production.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule now
&lt;/h2&gt;

&lt;p&gt;I still enjoy self-hosting. I am not interested in a purity test where everything must live on somebody else's cloud. But I am also done pretending that more containers automatically mean more skill.&lt;/p&gt;

&lt;p&gt;My rule now is simple: every service must earn the right to keep running.&lt;/p&gt;

&lt;p&gt;If it saves time, protects data I care about, supports teaching or content work, or teaches me something I will reuse, it stays. If it mostly produces updates, alerts, and extra surface area, it goes.&lt;/p&gt;

&lt;p&gt;That shift changed the feel of my homelab completely. It is calmer. Easier to recover. Easier to explain. Easier to leave alone for a few days without wondering what is quietly breaking.&lt;/p&gt;

&lt;p&gt;That is what sustainable self-hosting looks like. Not the biggest stack. The one you can still live with.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>docker</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Six arguing AI agents: what multi-agent debate teaches CS students about AI architecture</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:58:14 +0000</pubDate>
      <link>https://dev.to/techiemike/six-arguing-ai-agents-what-multi-agent-debate-teaches-cs-students-about-ai-architecture-5614</link>
      <guid>https://dev.to/techiemike/six-arguing-ai-agents-what-multi-agent-debate-teaches-cs-students-about-ai-architecture-5614</guid>
      <description>&lt;h1&gt;
  
  
  Six arguing AI agents: what multi-agent debate teaches CS students about AI architecture
&lt;/h1&gt;

&lt;p&gt;Most students meet AI through prompts. Type a question, get a paragraph back, move on.&lt;/p&gt;

&lt;p&gt;That framing is useful for five minutes and then it gets in the way.&lt;/p&gt;

&lt;p&gt;The more interesting lesson is what happens when you stop treating a model like a single all-knowing assistant and start treating it like one noisy component in a bigger system. Give several model instances the same problem. Ask each one for an answer. Make them criticise each other. Force another round. Then have a judge combine the best points.&lt;/p&gt;

&lt;p&gt;Suddenly the discussion stops being about prompt wording and starts being about architecture.&lt;/p&gt;

&lt;p&gt;That is why multi-agent debate matters. Not because six chat windows arguing with each other are cute, and not because debate turns an LLM into a reliable thinker. It matters because the pattern exposes what these systems are actually good at, where they fail, and what good computer science students should notice before they get hypnotised by demo videos.&lt;/p&gt;

&lt;p&gt;My take is blunt: prompt engineering is the least interesting part of this story. System design is the lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  What multi-agent debate actually is
&lt;/h2&gt;

&lt;p&gt;The 2023 paper &lt;a href="https://arxiv.org/abs/2305.14325" rel="noopener noreferrer"&gt;"Improving Factuality and Reasoning in Language Models through Multiagent Debate"&lt;/a&gt; describes a simple setup: multiple language model instances propose answers, debate them over several rounds, and then converge on a final answer. The authors report improvements on mathematical and strategic reasoning tasks, along with reductions in fallacious and hallucinatory responses compared with single-agent baselines.&lt;/p&gt;

&lt;p&gt;The idea is not mystical. You are building a feedback loop.&lt;/p&gt;

&lt;p&gt;A basic debate pipeline usually looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85b2ejvdslah3ur14911.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85b2ejvdslah3ur14911.webp" alt="Multi-agent debate pipeline diagram showing several agents answering the same prompt, critiquing each other, revising, and feeding a final judge" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent A, B, and C each answer the same prompt independently.&lt;/li&gt;
&lt;li&gt;Each agent reads the others' answers and points out weak logic, missing steps, or contradictions.&lt;/li&gt;
&lt;li&gt;Each agent revises its own answer after seeing criticism.&lt;/li&gt;
&lt;li&gt;A final judge, or one more synthesis pass, picks the strongest parts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That workflow borrows from something humans already do in programming: code review. One developer writes the first pass. Another person spots the broken assumption. Someone else notices the edge case. The final version is better, not because one brain became smarter, but because the process exposed mistakes before they shipped.&lt;/p&gt;

&lt;p&gt;If you have read my piece on &lt;a href="https://www.techiemike.com/the-chat-graveyard-how-to-export-search-and-learn-from-your-ai-conversations/" rel="noopener noreferrer"&gt;searching old AI conversations&lt;/a&gt;, the same pattern shows up there too. The value is often in the review loop, not the first output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What debate reveals about LLM architecture
&lt;/h2&gt;

&lt;p&gt;This is the part students should care about.&lt;/p&gt;

&lt;p&gt;A large language model is still an autoregressive system that predicts the next token from context. The &lt;a href="https://arxiv.org/abs/2005.14165" rel="noopener noreferrer"&gt;GPT-3 paper&lt;/a&gt; describes it as an autoregressive language model, and the 2022 paper &lt;a href="https://arxiv.org/abs/2201.11903" rel="noopener noreferrer"&gt;"Chain-of-Thought Prompting Elicits Reasoning in Large Language Models"&lt;/a&gt; showed that giving models room to produce intermediate reasoning steps can improve results on arithmetic, commonsense, and symbolic tasks. Multi-agent debate pushes that same idea one step further: instead of one long reasoning trace, you create several traces and let them attack each other.&lt;/p&gt;

&lt;p&gt;That tells you something important.&lt;/p&gt;

&lt;p&gt;A lot of what people call "reasoning" in practice is not a hidden mode that switches on inside the model. It is often the result of structure wrapped around the model: extra steps, extra context, criticism, retries, and selection. In other words, the intelligence you see is partly in the orchestration.&lt;/p&gt;

&lt;p&gt;I think students need to hear that early, because the internet keeps selling the opposite story. One flashy demo appears, people act as if the model has crossed some mysterious line, and the system design vanishes from the conversation.&lt;/p&gt;

&lt;p&gt;But look at what debate is doing under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redundancy, because several agents attempt the task&lt;/li&gt;
&lt;li&gt;error detection, because agents inspect each other's reasoning&lt;/li&gt;
&lt;li&gt;iterative refinement, because answers are revised instead of accepted once&lt;/li&gt;
&lt;li&gt;aggregation, because a judge or synthesis pass chooses among alternatives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are architecture decisions. They are not prompt tricks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9mc9ohpw7cl72z1vwj9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9mc9ohpw7cl72z1vwj9.webp" alt="LLM architecture feedback loop illustration showing redundancy, critique, iterative refinement, and aggregation in a multi-agent system" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is also why multi-agent systems look a lot like classic CS ideas wearing new clothes. Distributed systems use redundancy because one node can fail. Compilers use multiple passes because one pass is not always enough. Software teams use tests and review because the first version is rarely the final one. Debate is the same instinct applied to a probabilistic text engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful lesson is not "AI thinks better in groups"
&lt;/h2&gt;

&lt;p&gt;Be careful with the headline version of this idea.&lt;/p&gt;

&lt;p&gt;It is tempting to say that a group of agents "thinks" better than one agent. I would phrase it more carefully. A structured group process can produce better outputs than a single pass because it creates more chances to catch obvious mistakes.&lt;/p&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;If three weak answers all share the same bad assumption, debate does not magically rescue you. It can even make the wrong answer sound more convincing, because now it arrives with consensus theatre attached. Agreement is not proof. A polished synthesis is not proof either.&lt;/p&gt;

&lt;p&gt;That is one reason I still prefer grounded workflows over pure discussion workflows. The 2022 paper &lt;a href="https://arxiv.org/abs/2210.03629" rel="noopener noreferrer"&gt;"ReAct: Synergizing Reasoning and Acting in Language Models"&lt;/a&gt; is useful here because it combines reasoning traces with actions that fetch external information. For factual tasks, a debating panel with no access to evidence is still trapped inside its own guesses.&lt;/p&gt;

&lt;p&gt;So yes, debate can help. No, it does not turn a language model into a truth machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CS students should care
&lt;/h2&gt;

&lt;p&gt;If I were teaching this to a class, I would not present multi-agent debate as an AI party trick. I would use it to teach systems thinking.&lt;/p&gt;

&lt;p&gt;Students usually start with the surface layer: the prompt. That is understandable. It is the part they can see.&lt;/p&gt;

&lt;p&gt;But the deeper question is this: what kind of system are you building around an unreliable component?&lt;/p&gt;

&lt;p&gt;That question is pure computer science.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It teaches decomposition
&lt;/h3&gt;

&lt;p&gt;A decent multi-agent workflow splits one job into smaller responsibilities: proposer, critic, judge, verifier. That is exactly how good software systems are designed. Different parts do different jobs. Nobody asks the same component to generate, verify, rank, and explain everything perfectly in one pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It teaches evaluation
&lt;/h3&gt;

&lt;p&gt;Students often stop at "the AI answered." Debate forces the next question: answered according to what standard?&lt;/p&gt;

&lt;p&gt;Was the logic complete? Did it use evidence? Did two agents agree for the same reason, or did one just copy the other? What would count as a failure case?&lt;/p&gt;

&lt;p&gt;Those are evaluation questions, and they matter more than the prompt itself. I made a similar point in my guide on &lt;a href="https://www.techiemike.com/how-to-set-up-a-test-pipeline-that-actually-catches-bugs/" rel="noopener noreferrer"&gt;how to set up a test pipeline that actually catches bugs&lt;/a&gt;, because good systems need clear checks, not vibes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It teaches observability
&lt;/h3&gt;

&lt;p&gt;One reason single-shot AI tools are slippery is that you only see the final sentence. Debate systems expose more of the path. You can inspect the initial answers, the objections, the revisions, and the final merge. That makes the system easier to debug.&lt;/p&gt;

&lt;p&gt;It is the same reason tracebacks matter in programming. If you only saw "program failed," you would learn very little. If you can inspect the chain of events, you can actually reason about the bug. That is why I keep pushing students toward explicit debugging habits instead of one-shot guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It teaches trade-offs
&lt;/h3&gt;

&lt;p&gt;Debate sounds clever until you pay for it.&lt;/p&gt;

&lt;p&gt;Three rounds with four agents is already twelve model calls before the judge speaks. Latency goes up. Cost goes up. Failure modes multiply. Prompt leakage between agents becomes a design issue. Logging becomes a design issue. Rate limits become a design issue.&lt;/p&gt;

&lt;p&gt;That is good news for a CS classroom, because now the conversation is finally real. Students are not just asking, "Can AI do it?" They are asking, "What did this architecture cost, and was the gain worth it?"&lt;/p&gt;

&lt;p&gt;That is a much better question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three debate frameworks students can try
&lt;/h2&gt;

&lt;p&gt;You do not need a research lab to experiment with this. You just need to keep the tasks small and the scoring clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-agent critic loop
&lt;/h3&gt;

&lt;p&gt;This is the simplest version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent 1 writes an answer.&lt;/li&gt;
&lt;li&gt;Agent 2 critiques it line by line.&lt;/li&gt;
&lt;li&gt;Agent 1 revises once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use it for short explanations, code comments, or a paragraph answering a theory question. Students learn quickly that the second pass is often less about brilliance and more about catching omissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three-agent committee
&lt;/h3&gt;

&lt;p&gt;Give the same question to three isolated agents. Do not let them see each other at first. Then ask a judge to compare them against a rubric.&lt;/p&gt;

&lt;p&gt;This works well for structured tasks: design choices, algorithm explanations, short essays, and worked examples. The point is not to vote blindly. The point is to inspect disagreement. When two answers agree and one dissents, students should ask why.&lt;/p&gt;

&lt;h3&gt;
  
  
  The evidence-first debate
&lt;/h3&gt;

&lt;p&gt;This is the one I would push hardest.&lt;/p&gt;

&lt;p&gt;Before any agent can argue, each one has to quote or retrieve evidence from an approved source set: lecture notes, a paper extract, a dataset, or a fixed documentation page. Only then can the critique round begin.&lt;/p&gt;

&lt;p&gt;That rule stops the exercise from turning into a confidence contest. It also teaches a habit students badly need in the AI era: separate fluent output from justified output.&lt;/p&gt;

&lt;p&gt;If students want to run these experiments locally, they do not need an expensive machine. Smaller local setups are enough for classroom-scale tests, especially if the tasks are short and the models are modest. I covered that trade-off in my guide on &lt;a href="https://www.techiemike.com/running-ai-models-on-old-hardware-a-10-year-old-xeon-is-all-you-need-2/" rel="noopener noreferrer"&gt;running AI models on old hardware&lt;/a&gt;. For experimentation, architecture matters more than brute force.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the hype gets ahead of reality
&lt;/h2&gt;

&lt;p&gt;This is where I part company with the more breathless AI commentary.&lt;/p&gt;

&lt;p&gt;When people show a multi-agent demo, they often imply that the magic comes from "more agents." I do not think that is the right takeaway. Past a certain point, adding more agents can just add more noise, more cost, and more nicely formatted nonsense.&lt;/p&gt;

&lt;p&gt;The better question is whether each added step has a job.&lt;/p&gt;

&lt;p&gt;Does one agent bring independent evidence?&lt;br&gt;
Does one agent act as a strict verifier?&lt;br&gt;
Does one round of critique catch a failure mode you have actually observed?&lt;/p&gt;

&lt;p&gt;If the answer is no, you are probably building theatre.&lt;/p&gt;

&lt;p&gt;I see the same issue in some local AI tooling conversations too. People pile on frameworks because the stack looks impressive, not because each layer solves a real problem. That is why I keep coming back to practical architecture choices in posts like &lt;a href="https://www.techiemike.com/learning-docker-by-building-a-container-engine-from-scratch/" rel="noopener noreferrer"&gt;Learning Docker by Building a Container Engine from Scratch&lt;/a&gt;. More moving parts only help when each part earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would want students to remember
&lt;/h2&gt;

&lt;p&gt;If this topic lands anywhere useful in a CS classroom, it should land here.&lt;/p&gt;

&lt;p&gt;A language model is not the whole system.&lt;/p&gt;

&lt;p&gt;The prompt is not the whole system.&lt;/p&gt;

&lt;p&gt;The benchmark screenshot is definitely not the whole system.&lt;/p&gt;

&lt;p&gt;What matters is how you handle uncertainty, error checking, evidence, revision, and cost around a component that is fluent but unreliable. Multi-agent debate is worth studying because it makes those design choices visible.&lt;/p&gt;

&lt;p&gt;That is the lesson I would argue for: AI architecture is a systems problem first, and a wording problem second.&lt;/p&gt;

&lt;p&gt;A simple classroom version is to give three agents the same algorithm explanation, ask each one to identify the weakest assumption in the others' answers, then have students compare the final synthesis against a rubric. That keeps the lesson concrete. Students can see whether the extra critique round exposed a real weakness or just produced more polished noise.&lt;/p&gt;

&lt;p&gt;Students who understand that will build better tools, ask better questions, and fall for fewer demos.&lt;/p&gt;

&lt;p&gt;That seems more useful than teaching them how to write one more clever prompt.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>education</category>
      <category>architecture</category>
    </item>
    <item>
      <title>TLS Certificates for Internal Services: HTTPS Inside Your Homelab</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Sat, 11 Jul 2026 04:35:52 +0000</pubDate>
      <link>https://dev.to/techiemike/tls-certificates-for-internal-services-https-inside-your-homelab-12cj</link>
      <guid>https://dev.to/techiemike/tls-certificates-for-internal-services-https-inside-your-homelab-12cj</guid>
      <description>&lt;h1&gt;
  
  
  TLS Certificates for Internal Services: HTTPS Inside Your Homelab
&lt;/h1&gt;

&lt;p&gt;Most homelab TLS setups stop at the front door.&lt;/p&gt;

&lt;p&gt;You put Caddy, Nginx, or Traefik in front, get a valid certificate for &lt;code&gt;app.example.com&lt;/code&gt;, and call it done. From the browser to the reverse proxy, everything is encrypted. From the reverse proxy to the actual container, it is often plain HTTP on port 80 or 8080.&lt;/p&gt;

&lt;p&gt;That is not automatically wrong. On a single mini PC with a couple of containers, internal TLS is rarely the first problem I would fix. Weak passwords, overexposed dashboards, and lazy network rules usually matter more.&lt;/p&gt;

&lt;p&gt;But once a homelab grows beyond "one box, three containers," plain HTTP starts spreading everywhere. A reverse proxy talks to one VM over HTTP. A helper container calls another API with no encryption. A dashboard sends session cookies across the LAN because "it's only internal." That is the point where internal TLS stops being cosmetic and starts being decent housekeeping.&lt;/p&gt;

&lt;p&gt;My view is simple: if a service is worth exposing through a nice hostname, it is probably worth deciding how you want to trust it internally too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where plain HTTP sneaks back in
&lt;/h2&gt;

&lt;p&gt;The usual pattern looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser to reverse proxy: HTTPS&lt;/li&gt;
&lt;li&gt;Reverse proxy to upstream app: HTTP&lt;/li&gt;
&lt;li&gt;App to database: whatever the default was&lt;/li&gt;
&lt;li&gt;Admin tool to API endpoint: plain HTTP because it lives on the same subnet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can be acceptable in a tiny setup where everything runs on one host and nothing else can reach the traffic path. I still would not call it a long-term design. The moment you add another VM, another Docker host, or a tunnel back into the network, your "internal only" assumption gets weaker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8704xo9ud9u5mxfwlv5c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8704xo9ud9u5mxfwlv5c.webp" alt="Diagram showing browser-to-proxy HTTPS, proxy-to-app HTTP, and internal service traffic paths inside a homelab" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This came up for me while thinking about the same problem I discussed in my &lt;a href="https://www.techiemike.com/ollama-security-self-hosted-ai/" rel="noopener noreferrer"&gt;self-hosted AI security guide&lt;/a&gt;. Once you start putting tools behind reverse proxies and opening remote admin paths, the old habit of "just use HTTP inside" sticks around long after the setup stopped being small.&lt;/p&gt;

&lt;p&gt;Internal TLS does not make a homelab secure. It does do three practical things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It reduces casual packet snooping on segments you do not fully trust.&lt;/li&gt;
&lt;li&gt;It lets you test the same HTTPS assumptions internally that you expect externally.&lt;/li&gt;
&lt;li&gt;It stops you from building workflows that silently depend on insecure defaults.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third one matters more than people admit. If your automation only works when certificate checks are disabled, you have already taught your stack a bad habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three approaches that actually make sense
&lt;/h2&gt;

&lt;p&gt;I would ignore the endless certificate-tool rabbit hole and stick to three realistic options.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Local CA for quick wins
&lt;/h3&gt;

&lt;p&gt;If your goal is "I want valid-looking HTTPS inside the lab by tonight," a local CA is the low-friction option.&lt;/p&gt;

&lt;p&gt;Caddy has a &lt;code&gt;local_certs&lt;/code&gt; option that tells it to issue certificates internally instead of using a public ACME CA. That makes it useful for development environments and small private networks where you control the clients. The catch is the same as every private CA setup: the clients have to trust your root certificate first.&lt;/p&gt;

&lt;p&gt;This is the best fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you mostly browse services from your own machines&lt;/li&gt;
&lt;li&gt;you control the phones, laptops, or tablets that need trust installed&lt;/li&gt;
&lt;li&gt;you want HTTPS warnings gone without involving public DNS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the worst fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;guests or unmanaged devices need access&lt;/li&gt;
&lt;li&gt;you do not want to distribute a root CA to every client&lt;/li&gt;
&lt;li&gt;you expect native apps to behave nicely with an untrusted chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, local CA is tidy for a personal lab, messy for a shared one.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Let's Encrypt with DNS-01 for services under your real domain
&lt;/h3&gt;

&lt;p&gt;If your internal services already use names under a public domain you control, DNS-01 is usually the cleanest answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge" rel="noopener noreferrer"&gt;Let's Encrypt's DNS-01 challenge docs&lt;/a&gt; explain that your ACME client places a TXT record under &lt;code&gt;_acme-challenge&lt;/code&gt; for the domain. Those docs also note two details that matter here: DNS-01 can issue wildcard certificates, and it only makes sense when your DNS provider has an API you can automate.&lt;/p&gt;

&lt;p&gt;That means you can issue certificates for names under your domain without exposing the service itself on the public internet, as long as you can update DNS records for the validation step.&lt;/p&gt;

&lt;p&gt;This is the setup I would reach for if you already use a real domain for your homelab and want browser-trusted certificates without teaching every client to trust a private root.&lt;/p&gt;

&lt;p&gt;It works especially well with reverse proxies that already understand ACME. Traefik's ACME docs explicitly support &lt;code&gt;dnsChallenge&lt;/code&gt;, and the same docs note that ACME v2 wildcard certificates require a DNS-01 challenge.&lt;/p&gt;

&lt;p&gt;The downside is operational, not conceptual. You now have DNS API credentials in the mix. That is manageable, but it deserves the same care I talked about in the &lt;a href="https://www.techiemike.com/cloudflare-self-managed-oauth-homelab-security/" rel="noopener noreferrer"&gt;Cloudflare self-managed OAuth post&lt;/a&gt;: narrowly scoped credentials beat one giant token that can edit everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Private ACME with step-ca when the lab stops being small
&lt;/h3&gt;

&lt;p&gt;If you have multiple machines, multiple services, and want certificates to renew automatically without public DNS hacks, this is where &lt;code&gt;step-ca&lt;/code&gt; starts making sense.&lt;/p&gt;

&lt;p&gt;The useful part is not just "run your own CA." The &lt;a href="https://smallstep.com/docs/step-ca/acme-basics/" rel="noopener noreferrer"&gt;step-ca ACME basics docs&lt;/a&gt; show that &lt;code&gt;step-ca&lt;/code&gt; supports ACME, so standard ACME clients can request certificates from your own certificate authority instead of from a public provider. That means you can keep the familiar issuance flow while staying fully inside your own trust model.&lt;/p&gt;

&lt;p&gt;This is the option I would choose when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you have several hosts, not just several containers&lt;/li&gt;
&lt;li&gt;internal service names are not part of public DNS&lt;/li&gt;
&lt;li&gt;you want one internal CA instead of one-off self-signed certs everywhere&lt;/li&gt;
&lt;li&gt;you are prepared to manage trust distribution properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also the point where you need to be honest with yourself. A private CA is not hard because of certificate issuance. It is hard because every client needs the root certificate installed correctly, and you need a plan for renewal, revocation, and what happens when you rebuild devices.&lt;/p&gt;

&lt;p&gt;If that sounds annoying, that is because PKI is annoying. The tooling got better. The responsibility did not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8841599fm40cnu6i61w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8841599fm40cnu6i61w.webp" alt="Comparison chart showing three internal TLS options for a homelab: local CA, Let's Encrypt DNS-01, and private ACME with step-ca" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule of thumb
&lt;/h2&gt;

&lt;p&gt;Here is the version I would give to anyone building a real homelab instead of a PKI hobby project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One box, mostly personal devices, low stakes: local CA is fine.&lt;/li&gt;
&lt;li&gt;Public domain already in use, want normal browser trust: Let's Encrypt with DNS-01.&lt;/li&gt;
&lt;li&gt;Several hosts, several internal names, long-term setup: private ACME with step-ca.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I would not do is collect five half-solutions at once.&lt;/p&gt;

&lt;p&gt;I have seen a lot of homelabs end up with one self-signed cert for Grafana, a reverse proxy wildcard from Let's Encrypt, one app running plain HTTP forever, and a note to "fix later" on the service that matters most. That is not a design. That is archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test before you call it done
&lt;/h2&gt;

&lt;p&gt;After you flip a service to HTTPS, test the boring parts.&lt;/p&gt;

&lt;p&gt;These examples are worth keeping around:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
curl -vk https://service.example.internal/
openssl s_client -connect service.example.internal:443 -servername service.example.internal 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>security</category>
      <category>linux</category>
      <category>devops</category>
      <category>selfhosting</category>
    </item>
    <item>
      <title>Beyond Ollama: 3 Homelab AI Tools Worth Trying Next</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Fri, 10 Jul 2026 06:05:29 +0000</pubDate>
      <link>https://dev.to/techiemike/beyond-ollama-3-homelab-ai-tools-worth-trying-next-4ai9</link>
      <guid>https://dev.to/techiemike/beyond-ollama-3-homelab-ai-tools-worth-trying-next-4ai9</guid>
      <description>&lt;h1&gt;
  
  
  Beyond Ollama: 3 Homelab AI Tools Worth Trying Next
&lt;/h1&gt;

&lt;p&gt;Ollama is the tool I recommend when someone wants their first local model running tonight. It is simple, fast to understand, and good enough for a lot of homelab jobs. Pull a model, hit an API, wire up Open WebUI, done. I already covered that full path in my &lt;a href="https://www.techiemike.com/self-hosted-llms-with-docker-ollama-and-open-webui/" rel="noopener noreferrer"&gt;Docker + Ollama + Open WebUI guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But Ollama also nudges you toward one shape of local AI: text models behind a neat API. That is fine until you want more control over image workflows, a cleaner OpenAI-compatible service layer, or one local runtime that can cover chat, speech, embeddings, and image generation from the same box.&lt;/p&gt;

&lt;p&gt;That is where the next wave gets interesting.&lt;/p&gt;

&lt;p&gt;The three projects I would watch right now are ComfyUI, LocalAI, and Lemonade. They are not direct replacements for Ollama in the sense of "uninstall Ollama and forget it exists." They solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ComfyUI is for visual AI workflows where you want to control every step&lt;/li&gt;
&lt;li&gt;LocalAI is for homelabbers who want a broader local API surface with OpenAI-compatible apps&lt;/li&gt;
&lt;li&gt;Lemonade is for people who want to experiment with a broader local AI runtime with chat, image, speech, and standard APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My view is simple: Ollama is still the easiest first stop, but it is no longer the whole conversation. If your homelab AI stack stops at Ollama, you are missing some genuinely useful tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3xlkkzfnbrqor83vh6l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy3xlkkzfnbrqor83vh6l.webp" alt="Comparison graphic showing Ollama as the starting point, then branching into ComfyUI for visual workflows, LocalAI for API compatibility, and Lemonade for multimodal local services" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why look past Ollama at all?
&lt;/h2&gt;

&lt;p&gt;Ollama does one thing very well: it makes local LLM inference approachable. That matters. A lot of local AI users never move past the "I want a model on my machine" stage, and they do not need to.&lt;/p&gt;

&lt;p&gt;Still, three limits show up pretty quickly.&lt;/p&gt;

&lt;p&gt;First, Ollama is mostly about model serving, not workflow design. If you want to build a repeatable image pipeline with prompt branches, upscalers, masks, and post-processing, Ollama is the wrong tool.&lt;/p&gt;

&lt;p&gt;Second, plenty of apps expect an OpenAI-style API and want more than text generation. LocalAI leans hard into that compatibility layer, and that can save time when you are wiring tools together.&lt;/p&gt;

&lt;p&gt;Third, local AI is getting more multimodal. Text alone is no longer the interesting part. Speech to text, text to speech, embeddings, image generation, and agent tooling are all creeping into the same workflows. Lemonade is appealing because it is trying to package that broader stack into one local service.&lt;/p&gt;

&lt;p&gt;That does not mean these projects are automatically better. It means they cover ground Ollama does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ComfyUI: the visual workflow engine
&lt;/h2&gt;

&lt;p&gt;If your local AI curiosity has moved from "run a model" to "build a pipeline," ComfyUI is the one to try next.&lt;/p&gt;

&lt;p&gt;ComfyUI's &lt;a href="https://docs.comfy.org/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; describes it as an AI creation engine for visual professionals who want control over every model, every parameter, and every output. The docs also push two ideas that matter for homelab users: you can run ComfyUI on your own hardware through Comfy Desktop, and the whole product is built around workflows you can load, remix, and iterate on.&lt;/p&gt;

&lt;p&gt;That workflow-first approach is the real difference.&lt;/p&gt;

&lt;p&gt;Instead of typing one prompt into a chat box and hoping for the best, you build a graph. One node loads a checkpoint. Another encodes the prompt. Another sets sampler settings. Another upscales. Another handles masks or ControlNet-style conditioning. It feels more like wiring a media pipeline than chatting with a bot.&lt;/p&gt;

&lt;p&gt;That is why ComfyUI matters beyond image hobbyists. It teaches a better mental model for local AI. You stop thinking in one-shot prompts and start thinking in reusable systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5o3ghmcsc1bkcirjowqz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5o3ghmcsc1bkcirjowqz.webp" alt="Workflow diagram showing a ComfyUI node graph for local image generation with checkpoint loading, prompt encoding, upscaling, and output stages" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What ComfyUI is good at
&lt;/h3&gt;

&lt;p&gt;For a homelab, I think ComfyUI makes sense in four cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You want stable, repeatable image generation workflows instead of random prompting.&lt;/li&gt;
&lt;li&gt;You are generating assets for a blog, course, or small business and you need consistency.&lt;/li&gt;
&lt;li&gt;You want to understand how visual AI pipelines actually fit together.&lt;/li&gt;
&lt;li&gt;You want to save and reuse workflows instead of rebuilding the same prompt stack every time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The community workflow library is a big part of the appeal. You can start from a template, inspect how it works, then trim it down to fit your own hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where ComfyUI gets awkward
&lt;/h3&gt;

&lt;p&gt;The downside is also obvious: ComfyUI asks more from you.&lt;/p&gt;

&lt;p&gt;If Ollama feels like &lt;code&gt;docker run&lt;/code&gt; plus a chat window, ComfyUI feels like learning a node editor. That is not a flaw. It is the point. But it means the learning curve is real.&lt;/p&gt;

&lt;p&gt;It is also the least forgiving option here if your hardware is modest and your expectations are not. Text inference on CPU-only hardware can still be useful, as I found in my &lt;a href="https://www.techiemike.com/running-ai-models-on-old-hardware-a-10-year-old-xeon-is-all-you-need-2/" rel="noopener noreferrer"&gt;old hardware local AI write-up&lt;/a&gt;. Image workflows are different. You can run them locally, but speed and model choice matter a lot more.&lt;/p&gt;

&lt;p&gt;So my practical advice is this: do not start ComfyUI by chasing the flashiest workflow on social media. Start with a basic community workflow, a smaller model, and one job you actually have. Blog art. Thumbnail variations. Product mockups. Something concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  My homelab take on ComfyUI
&lt;/h3&gt;

&lt;p&gt;ComfyUI is the tool in this list that most rewards a dedicated box or VM. If your homelab has a machine with a decent GPU, this is where it earns its keep. If your homelab is just a mini PC with integrated graphics and limited RAM, ComfyUI can still be educational, but I would not make it your first "serious production" local AI service.&lt;/p&gt;

&lt;p&gt;That is also why I do not see ComfyUI as an Ollama replacement. I see it as the moment local AI stops being only about chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. LocalAI: the compatibility play
&lt;/h2&gt;

&lt;p&gt;LocalAI is interesting for a completely different reason.&lt;/p&gt;

&lt;p&gt;Its homepage makes the pitch very clearly: it is a free OpenAI and Anthropic alternative, built as a small composable AI stack. The &lt;a href="https://localai.io/basics/getting_started/index.html" rel="noopener noreferrer"&gt;LocalAI quickstart&lt;/a&gt; also states that it is a drop-in replacement for the OpenAI API, supports multiple model families, and can run language models, images, audio, and more locally. Docker is the recommended installation method, and the quick start is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;--name&lt;/span&gt; local-ai &lt;span class="nt"&gt;-ti&lt;/span&gt; localai/localai:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single detail tells you who LocalAI is for.&lt;/p&gt;

&lt;p&gt;It is for the homelabber who already has a pile of tools that expect a familiar API shape and does not want to keep adapting everything around one runtime. If you have scripts, agent tools, or self-hosted apps that speak OpenAI-style endpoints, LocalAI can be easier to slot into place than a more opinionated stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  What LocalAI is good at
&lt;/h3&gt;

&lt;p&gt;The best part of LocalAI is not that it tries to do everything. It is that it tries to expose local models through interfaces a lot of existing software already understands.&lt;/p&gt;

&lt;p&gt;That gives it three obvious uses in a homelab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replacing cloud API calls in tools that already support OpenAI-compatible backends&lt;/li&gt;
&lt;li&gt;centralising local inference behind one service instead of a stack of ad hoc wrappers&lt;/li&gt;
&lt;li&gt;experimenting with broader local AI features without locking yourself into a single app UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also like the way the project frames itself as composable. That usually matters more in a homelab than polished marketing. You want pieces you can fit into your own stack, not a black box that wants to own the whole machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part people gloss over
&lt;/h3&gt;

&lt;p&gt;OpenAI-compatible does not mean identical behaviour across every app.&lt;/p&gt;

&lt;p&gt;This is where a lot of local AI blog posts get sloppy. A compatible API gets you much closer to plug-and-play, but model capabilities, latency, backend support, and edge-case behaviour still vary. The docs promise the API shape. They do not promise every upstream app will behave exactly as if it were talking to OpenAI.&lt;/p&gt;

&lt;p&gt;That is not a criticism of LocalAI. It is just the honest version.&lt;/p&gt;

&lt;p&gt;If you are the kind of homelabber who likes clean interfaces and clear boundaries, LocalAI is a strong fit. If you want a dead-simple chat setup in ten minutes, Ollama is still easier. If you want a broad local API target for apps and experiments, LocalAI starts to look better.&lt;/p&gt;

&lt;h3&gt;
  
  
  My homelab take on LocalAI
&lt;/h3&gt;

&lt;p&gt;This is the one I would try first if I were moving from "local toy" to "local platform."&lt;/p&gt;

&lt;p&gt;Why? Because a homelab gets messy fast. One service wants text generation. Another wants embeddings. Another expects an OpenAI-style endpoint. Another needs audio later. A compatibility layer matters more than people think.&lt;/p&gt;

&lt;p&gt;I would still keep the deployment conservative. Bind it locally first. Put a reverse proxy and auth in front of it if you need remote access. The same local AI security rules I wrote about for Ollama still apply here: do not confuse "self-hosted" with "safe by default."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lemonade: the all-in-one runtime I would keep an eye on
&lt;/h2&gt;

&lt;p&gt;Lemonade is the newest-feeling project in this list, and it is the one I find easiest to imagine on a modern homelab.&lt;/p&gt;

&lt;p&gt;Its site pitches Lemonade as local AI for text, images, and speech. The installation flow says it equips your machine with an AI runtime, GUI, CLI, and API endpoints. The &lt;a href="https://github.com/lemonade-sdk/lemonade" rel="noopener noreferrer"&gt;Lemonade GitHub project&lt;/a&gt; pushes the same idea harder: Lemonade Server exposes standard OpenAI, Anthropic, and Ollama APIs, while the broader platform covers chat, coding, speech, image generation, transcription, and embeddings.&lt;/p&gt;

&lt;p&gt;For Docker users, the site shows this quick start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; lemonade-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 13305:13305 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; lemonade-cache:/root/.cache/huggingface &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; lemonade-llama:/opt/lemonade/llama &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/lemonade-sdk/lemonade-server:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a serious homelab signal. It tells me the project understands how people like us test things: one container, mapped port, persistent volumes, then decide whether it deserves a permanent home.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Lemonade is good at
&lt;/h3&gt;

&lt;p&gt;What I like here is the ambition.&lt;/p&gt;

&lt;p&gt;Lemonade is not saying "here is one local model runner." It is saying "here is a local-first AI runtime that can serve apps and agents, speak familiar APIs, and cover multiple modalities." The README even exposes CLI flows like &lt;code&gt;lemonade run&lt;/code&gt;, &lt;code&gt;lemonade pull&lt;/code&gt;, and &lt;code&gt;lemonade list&lt;/code&gt;, plus a built-in model manager.&lt;/p&gt;

&lt;p&gt;That makes Lemonade appealing for a homelab in three situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want one local runtime for more than chat&lt;/li&gt;
&lt;li&gt;you want a local service that can talk to existing apps over standard APIs&lt;/li&gt;
&lt;li&gt;you want multimodal experiments without stitching five unrelated projects together yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a quiet but important detail in the README: Lemonade documents support across CPU, GPU, and in some cases NPU-focused configurations depending on backend and platform. I would not promise all of that on every random mini PC. But the direction is interesting. It feels closer to where local AI is heading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I would still be cautious
&lt;/h3&gt;

&lt;p&gt;The more all-in-one a project becomes, the more you need to be realistic about scope.&lt;/p&gt;

&lt;p&gt;Broad capability claims are easy to love in a README and harder to operate in a homelab. Multimodal stacks usually mean more storage, more dependency churn, more model management, and more opportunities to end up debugging the runtime instead of using it.&lt;/p&gt;

&lt;p&gt;So I would treat Lemonade as a platform experiment before I treated it as core infrastructure.&lt;/p&gt;

&lt;p&gt;That is not an insult. Plenty of the best homelab tools start as platform experiments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl2v2xws9bfav09pco99g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl2v2xws9bfav09pco99g.webp" alt="Dashboard-style illustration comparing LocalAI and Lemonade as local API and multimodal runtime layers on homelab hardware" width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  My homelab take on Lemonade
&lt;/h3&gt;

&lt;p&gt;If I wanted one box in the rack to become "the local AI services machine," Lemonade is the project here that best matches that idea.&lt;/p&gt;

&lt;p&gt;Not because it is automatically the best at every individual task. It probably is not. But because it is trying to unify text, image, speech, and app-facing APIs into one local runtime. That is a more useful direction than yet another single-purpose model wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you choose?
&lt;/h2&gt;

&lt;p&gt;Short version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick ComfyUI if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;you care about image generation or visual workflows more than chat&lt;/li&gt;
&lt;li&gt;you want to build reusable pipelines, not just send prompts&lt;/li&gt;
&lt;li&gt;you have the patience to learn a node-based tool properly&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick LocalAI if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;you want a local OpenAI-compatible service for apps and scripts&lt;/li&gt;
&lt;li&gt;you are building a broader self-hosted AI platform, not just a chat box&lt;/li&gt;
&lt;li&gt;you prefer composable infrastructure over an all-in-one user experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick Lemonade if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;you want one local runtime that reaches beyond text&lt;/li&gt;
&lt;li&gt;you care about chat, speech, embeddings, and image generation in the same stack&lt;/li&gt;
&lt;li&gt;you like the idea of standard APIs plus local-first multimodal tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stick with Ollama if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;your real need is still just local text inference&lt;/li&gt;
&lt;li&gt;you want the smallest setup burden&lt;/li&gt;
&lt;li&gt;you value simplicity more than feature breadth right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. There is no prize for making your homelab more complicated than it needs to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical setup advice before you install any of them
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than choosing the "right" brand.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Separate experiments from core services
&lt;/h3&gt;

&lt;p&gt;Do not dump every AI runtime onto the same VM that already runs your password manager, DNS, and media stack. Give local AI tools their own box, LXC, or VM where possible. They pull big models, eat disk, and change quickly.&lt;/p&gt;

&lt;p&gt;If you are planning that separation now, my &lt;a href="https://www.techiemike.com/homelab-network-design-building-your-own-miniature-data-centre-at-home/" rel="noopener noreferrer"&gt;homelab network design guide&lt;/a&gt; is the post I would read before carving out another AI host.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start with one use case, not three
&lt;/h3&gt;

&lt;p&gt;The fastest way to waste a weekend is to install ComfyUI, LocalAI, and Lemonade in one sitting and learn none of them properly. Pick one actual job first.&lt;/p&gt;

&lt;p&gt;If the job is image workflows, use ComfyUI.&lt;br&gt;
If the job is API compatibility, use LocalAI.&lt;br&gt;
If the job is "I want a broader local AI service layer," try Lemonade.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Budget storage before you budget hype
&lt;/h3&gt;

&lt;p&gt;Model storage gets out of hand fast. Between checkpoints, embeddings, speech models, caches, and image assets, local AI can eat SSD space long before it maxes out CPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep security boring
&lt;/h3&gt;

&lt;p&gt;Local AI projects are still software stacks with ports, APIs, and update cycles. Keep them off the open internet unless you have a very good reason. Bind locally first. Add auth deliberately. Reuse the same caution you would use for any self-hosted dashboard or API. My &lt;a href="https://www.techiemike.com/ollama-security-self-hosted-ai/" rel="noopener noreferrer"&gt;Ollama security guide&lt;/a&gt; exists for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  My actual opinion
&lt;/h2&gt;

&lt;p&gt;I do not think the next step after Ollama is "the one true replacement." I think the better question is what kind of homelab AI user you are becoming.&lt;/p&gt;

&lt;p&gt;If you are becoming a workflow builder, ComfyUI makes sense.&lt;br&gt;
If you are becoming an infrastructure person, LocalAI makes sense.&lt;br&gt;
If you want a local-first service layer that feels closer to a real AI platform, Lemonade is the one I would watch.&lt;/p&gt;

&lt;p&gt;That is why this space is more interesting now than it was six months ago. Local AI is finally splitting into real categories instead of pretending every problem is solved by one model runner and a chat window.&lt;/p&gt;

&lt;p&gt;Ollama got a lot of people through the front door. Good. It needed to happen.&lt;/p&gt;

&lt;p&gt;But the room is bigger now.&lt;/p&gt;

&lt;p&gt;If you want the philosophy behind local-first AI, read &lt;a href="https://www.techiemike.com/right-to-local-intelligence/" rel="noopener noreferrer"&gt;Right to Local Intelligence&lt;/a&gt;. If you want the easy starting point, use Ollama. If you want the next layer in a homelab that is starting to do serious work, these three are where I would look next.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Ask for Help: Technical Communication Skills Every CS Student Needs</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Thu, 09 Jul 2026 02:01:19 +0000</pubDate>
      <link>https://dev.to/techiemike/how-to-ask-for-help-technical-communication-skills-every-cs-student-needs-1o4c</link>
      <guid>https://dev.to/techiemike/how-to-ask-for-help-technical-communication-skills-every-cs-student-needs-1o4c</guid>
      <description>&lt;h1&gt;
  
  
  How to Ask for Help: Technical Communication Skills Every CS Student Needs
&lt;/h1&gt;

&lt;p&gt;A student shows me a Python program and says, "It doesn't work."&lt;/p&gt;

&lt;p&gt;That is the whole bug report.&lt;/p&gt;

&lt;p&gt;No error message. No explanation of what the program is meant to do. No clue whether it ever worked, whether they changed anything, or whether they even ran it again after the last edit. I have to drag the story out of them one question at a time, like I'm interviewing a very tired witness.&lt;/p&gt;

&lt;p&gt;This is not a small classroom habit. It is a technical communication problem, and it follows students straight into university, internships, GitHub issues, team chats, and software jobs.&lt;/p&gt;

&lt;p&gt;Most CS courses spend a lot of time on syntax, algorithms, and debugging. They spend far less time on the moment just before debugging becomes collaborative: the point where you have to explain the problem to another human. That missing skill matters more than students realise. A clear help request saves time, gets better answers, and often helps you solve the problem before anyone replies.&lt;/p&gt;

&lt;p&gt;I would go further than that: I think a student who can describe a bug clearly is usually closer to being job-ready than a student who can memorise one more sorting algorithm but cannot explain what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this skill matters more than students think
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.cambridgeinternational.org/programmes-and-qualifications/cambridge-igcse-computer-science-0478/" rel="noopener noreferrer"&gt;Cambridge IGCSE Computer Science syllabus for 2026-2028&lt;/a&gt; says practical problem-solving and programming should be integral to the course, and that learners should have the opportunity to write, run, test, and debug their own programs.&lt;/p&gt;

&lt;p&gt;The human side is what happens when testing and debugging stall.&lt;/p&gt;

&lt;p&gt;If you cannot explain what you expected, what happened instead, and what you already tried, your teacher, classmate, or teammate starts from zero every time. Good questions lower that cost. Bad questions spread confusion.&lt;/p&gt;

&lt;p&gt;This also scales beyond school surprisingly fast. The same habits show up in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asking a teacher why your pseudocode answer lost marks&lt;/li&gt;
&lt;li&gt;posting on Stack Overflow&lt;/li&gt;
&lt;li&gt;filing a GitHub issue for a broken project&lt;/li&gt;
&lt;li&gt;asking for help in a company Slack channel&lt;/li&gt;
&lt;li&gt;handing over a bug to another developer during a team project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this is not just "how to ask nicely." It is part of technical work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three mistakes that waste the most time
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Asking about the guess instead of the real problem
&lt;/h3&gt;

&lt;p&gt;This is the classic XY problem.&lt;/p&gt;

&lt;p&gt;The short version is simple: you want to solve X, you get stuck, you guess that Y might be the way to do it, then you ask for help with Y instead of explaining X. The person helping you sees a strange sideways question and has no idea what you are actually trying to achieve.&lt;/p&gt;

&lt;p&gt;A student version of this sounds like:&lt;/p&gt;

&lt;p&gt;"How do I make Python ignore line 14?"&lt;/p&gt;

&lt;p&gt;Maybe line 14 is not the real issue. Maybe the real issue is that the student is trying to skip invalid input, or they misunderstood a loop condition, or they are patching around data that should have been cleaned earlier.&lt;/p&gt;

&lt;p&gt;If you only ask about the guessed fix, people can spend ten minutes helping with the wrong thing.&lt;/p&gt;

&lt;p&gt;I see this constantly when students jump straight to a supposed solution instead of describing the goal. They ask, "How do I stop this traceback from showing?" when the real question is, "Why is my file not opening?" They ask, "How do I force this variable to become an integer?" when the real question is, "Why is my comparison failing?"&lt;/p&gt;

&lt;p&gt;If you remember one rule from this post, make it this one: explain the actual task before you explain your attempted fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Being too vague
&lt;/h3&gt;

&lt;p&gt;"My code is wrong" is not a useful starting point.&lt;/p&gt;

&lt;p&gt;Neither is "Please help asap" or "Why doesn't this work?"&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://stackoverflow.com/help/how-to-ask" rel="noopener noreferrer"&gt;Stack Overflow help page on asking good questions&lt;/a&gt; still pushes the same basic idea it has pushed for years: search first, then ask a question that summarises the specific problem. That works because specific questions are answerable. Vague questions are not.&lt;/p&gt;

&lt;p&gt;A vague question forces the helper to guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what language or tool you are using&lt;/li&gt;
&lt;li&gt;what output you expected&lt;/li&gt;
&lt;li&gt;what output you got&lt;/li&gt;
&lt;li&gt;whether there was an error message&lt;/li&gt;
&lt;li&gt;whether the bug happens every time&lt;/li&gt;
&lt;li&gt;whether your code is complete enough to run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time all of that gets clarified, the conversation has already become slower and more irritating than it needed to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hiding the context and the attempt
&lt;/h3&gt;

&lt;p&gt;Students sometimes think showing failed attempts makes them look weak. In reality, hiding them makes the question harder to answer.&lt;/p&gt;

&lt;p&gt;What you tried is part of the technical evidence.&lt;/p&gt;

&lt;p&gt;If you already tested two different loops, changed the file path, and confirmed the CSV exists, that matters. It stops people suggesting the same dead ends again. It also proves you are not asking someone else to do the whole task for you.&lt;/p&gt;

&lt;p&gt;The same goes for error messages. Do not paraphrase them into mush.&lt;/p&gt;

&lt;p&gt;"Python had some syntax problem" is far less useful than pasting the actual traceback and saying which line you think it points to.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple framework for writing a good help request
&lt;/h2&gt;

&lt;p&gt;When students ask me for a template, I give them four parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Context
&lt;/h3&gt;

&lt;p&gt;Start with what you are trying to build or do.&lt;/p&gt;

&lt;p&gt;Not your guessed fix. The real task.&lt;/p&gt;

&lt;p&gt;Good:&lt;br&gt;
"I'm writing a Python program for an IGCSE-style file handling task. It should read names and marks from a text file, then print the highest mark."&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
"How do I make line 8 skip to line 12?"&lt;/p&gt;

&lt;p&gt;The first version gives the helper a map. The second throws them into a random alley.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What you expected
&lt;/h3&gt;

&lt;p&gt;Say what the program, query, script, or system should do.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
"I expected it to print the highest mark in the file, which should be 87."&lt;/p&gt;

&lt;p&gt;This sounds basic, but it matters. Debugging without a clear expected result turns into guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What actually happened
&lt;/h3&gt;

&lt;p&gt;Now give the observed behaviour.&lt;/p&gt;

&lt;p&gt;That could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact error message&lt;/li&gt;
&lt;li&gt;the wrong output&lt;/li&gt;
&lt;li&gt;no output&lt;/li&gt;
&lt;li&gt;a crash&lt;/li&gt;
&lt;li&gt;a hang&lt;/li&gt;
&lt;li&gt;inconsistent behaviour between runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be literal. Paste the traceback. Quote the output. Name the line number if you have one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. What you already tried
&lt;/h3&gt;

&lt;p&gt;Finish with the attempt.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
"I checked whether the file path was correct, printed each line to confirm the file was reading, and changed &lt;code&gt;highest = 0&lt;/code&gt; to &lt;code&gt;highest = int(lines[0])&lt;/code&gt;, but I still get a &lt;code&gt;ValueError&lt;/code&gt; on the blank final line."&lt;/p&gt;

&lt;p&gt;That sentence does two useful things. It shows effort, and it gives the helper a narrower search area.&lt;/p&gt;

&lt;p&gt;Put those four parts together and the question becomes much easier to answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwc9i7gnoewmsqs4spyw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwc9i7gnoewmsqs4spyw.webp" alt="Notebook diagram showing the four-part help request framework: context, expected result, actual result, and what I tried" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal reproducible example saves time
&lt;/h2&gt;

&lt;p&gt;On Stack Overflow, the standard advice is to provide a Minimal, Reproducible Example.&lt;/p&gt;

&lt;p&gt;Minimal means you cut the code down to only the part that still shows the problem.&lt;/p&gt;

&lt;p&gt;Complete means someone else has everything they need to run it.&lt;/p&gt;

&lt;p&gt;Reproducible means you tested it yourself and confirmed the problem still happens.&lt;/p&gt;

&lt;p&gt;It feels fiddly at first, but it pays off fast.&lt;/p&gt;

&lt;p&gt;When students trim a 140-line program down to 18 lines and the bug disappears, they often solve the problem before they even send the question. The act of isolating the bug is already a debugging method.&lt;/p&gt;

&lt;p&gt;It also makes helpers more willing to engage. Eighteen lines will get read far more often than a full coursework dump.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foel3d65lrg6lr9hh5ton.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foel3d65lrg6lr9hh5ton.webp" alt="Side-by-side comparison of a vague help request and a structured bug report with context, expected output, actual output, and attempts" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad question vs good question
&lt;/h2&gt;

&lt;p&gt;Here is the kind of message that usually goes nowhere:&lt;/p&gt;

&lt;p&gt;"My Python file handling code isn't working. Can someone fix it?"&lt;/p&gt;

&lt;p&gt;That gives the helper almost nothing.&lt;/p&gt;

&lt;p&gt;Now compare it with this:&lt;/p&gt;

&lt;p&gt;"I'm writing a Python program that reads student names and marks from a text file, then prints the highest mark. I expected it to print 87, but instead I get &lt;code&gt;ValueError: invalid literal for int() with base 10: ''&lt;/code&gt; on line 12. I tested the file path and printed each line, and I think the problem is the blank line at the end of the file. Here's the 14-line version that still causes the error."&lt;/p&gt;

&lt;p&gt;That second version is just structured.&lt;/p&gt;

&lt;p&gt;And structure is the whole game here.&lt;/p&gt;

&lt;p&gt;I would rather read a blunt, slightly messy question like that than a polished message that hides the real issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in a professional setting
&lt;/h2&gt;

&lt;p&gt;Students sometimes think this advice only applies to forums. It does not.&lt;/p&gt;

&lt;p&gt;In software work, asking for help usually becomes one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bug report&lt;/li&gt;
&lt;li&gt;a GitHub issue&lt;/li&gt;
&lt;li&gt;a pull request comment&lt;/li&gt;
&lt;li&gt;a handover note&lt;/li&gt;
&lt;li&gt;a message in a team chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub's own issue flow is built around the same basics: write a title, then describe the issue in the body. That is not bureaucracy. It is an attempt to make problems legible.&lt;/p&gt;

&lt;p&gt;A strong junior developer still gets stuck.&lt;/p&gt;

&lt;p&gt;A strong junior developer is someone who can say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what they were trying to do&lt;/li&gt;
&lt;li&gt;what environment they were using&lt;/li&gt;
&lt;li&gt;what steps reproduce the bug&lt;/li&gt;
&lt;li&gt;what they expected&lt;/li&gt;
&lt;li&gt;what they saw instead&lt;/li&gt;
&lt;li&gt;what they already checked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That person is easier to help, easier to trust, and easier to work with.&lt;/p&gt;

&lt;p&gt;This is one reason I keep pushing technical communication in CS classes. Industry does not reward silent confusion. It rewards people who can surface a problem clearly enough for a team to act on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist students can actually use
&lt;/h2&gt;

&lt;p&gt;Before you ask for help, check whether your message includes these points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What are you trying to do?&lt;/li&gt;
&lt;li&gt;What language, tool, or platform are you using?&lt;/li&gt;
&lt;li&gt;What did you expect to happen?&lt;/li&gt;
&lt;li&gt;What actually happened?&lt;/li&gt;
&lt;li&gt;What is the exact error message?&lt;/li&gt;
&lt;li&gt;What have you already tried?&lt;/li&gt;
&lt;li&gt;Can you shrink it to a minimal reproducible example?&lt;/li&gt;
&lt;li&gt;Are you asking about the real problem, or just your guessed fix?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two or three of those are missing, stop and rewrite the question.&lt;/p&gt;

&lt;p&gt;That rewrite is not wasted time. It is often the moment the bug becomes obvious.&lt;/p&gt;

&lt;p&gt;If you want practice reading error messages before you ask for help, my guide on &lt;a href="https://www.techiemike.com/debugging-python-for-a-level-cs-reading-tracebacks-and-finding-bugs/" rel="noopener noreferrer"&gt;https://www.techiemike.com/debugging-python-for-a-level-cs-reading-tracebacks-and-finding-bugs/&lt;/a&gt; is a good place to start. If you need a more structured approach to catching problems earlier, I also wrote about &lt;a href="https://www.techiemike.com/how-to-set-up-a-test-pipeline-that-actually-catches-bugs/" rel="noopener noreferrer"&gt;https://www.techiemike.com/how-to-set-up-a-test-pipeline-that-actually-catches-bugs/&lt;/a&gt;. And if you keep losing useful debugging context across chats and tabs, &lt;a href="https://www.techiemike.com/the-chat-graveyard-how-to-export-search-and-learn-from-your-ai-conversations/" rel="noopener noreferrer"&gt;https://www.techiemike.com/the-chat-graveyard-how-to-export-search-and-learn-from-your-ai-conversations/&lt;/a&gt; is worth bookmarking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that saves the most time
&lt;/h2&gt;

&lt;p&gt;The strongest students I teach are not always the fastest coders.&lt;/p&gt;

&lt;p&gt;They are usually the ones who stop, gather the evidence, and explain the bug properly.&lt;/p&gt;

&lt;p&gt;That sounds almost too simple, but it changes everything. Better questions lead to better answers. Better answers lead to faster debugging. Faster debugging leaves more time for the work that actually matters.&lt;/p&gt;

&lt;p&gt;So the next time your instinct is to type, "It doesn't work," do one more pass.&lt;/p&gt;

&lt;p&gt;State the task. Show the evidence. Explain what you tried. Ask the real question.&lt;/p&gt;

&lt;p&gt;That is not just asking for help.&lt;/p&gt;

&lt;p&gt;That is technical communication, and it is one of the most practical skills a CS student can build.&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>python</category>
      <category>debugging</category>
      <category>career</category>
    </item>
    <item>
      <title>Linux Pipes and Redirection: The Missing CS Lesson That Powers Real-World Automation</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Wed, 08 Jul 2026 02:01:16 +0000</pubDate>
      <link>https://dev.to/techiemike/linux-pipes-and-redirection-the-missing-cs-lesson-that-powers-real-world-automation-3jhl</link>
      <guid>https://dev.to/techiemike/linux-pipes-and-redirection-the-missing-cs-lesson-that-powers-real-world-automation-3jhl</guid>
      <description>&lt;h1&gt;
  
  
  Linux Pipes and Redirection: The Missing CS Lesson That Powers Real-World Automation
&lt;/h1&gt;

&lt;p&gt;A few weeks ago I was debugging why one of my Docker containers kept crashing. The logs were enormous — thousands of lines. I needed to find error messages, count how often they appeared, and figure out which service was the culprit, preferably sorted by frequency. What would have been 15 minutes of scrolling became this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs my-app 2&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; error | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f3-&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Seven commands chained together. The result was on my screen in under a second.&lt;/p&gt;

&lt;p&gt;If you're running Docker, understanding &lt;a href="https://www.techiemike.com/learning-docker-by-building-a-container-engine-from-scratch/" rel="noopener noreferrer"&gt;how containers actually work&lt;/a&gt; makes these pipelines more powerful — the same Linux primitives that power containers also power everything covered here.&lt;/p&gt;

&lt;p&gt;I teach Cambridge IGCSE and A-Level Computer Science. Chapter 4 of the IGCSE syllabus covers operating systems, including their role in providing an interface between users and hardware. The syllabus mentions that OSes provide an interface — in my classroom, we expand that into the CLI vs. GUI distinction. What students don't learn is &lt;em&gt;how&lt;/em&gt; — the mechanism that makes the CLI more than a slower, uglier version of a GUI.&lt;/p&gt;

&lt;p&gt;That mechanism is pipes and redirection. It's the thing that transforms a terminal from a historical curiosity into an automation engine. And it's missing from every CS syllabus I've taught.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Descriptors: The Invisible Plumbing
&lt;/h2&gt;

&lt;p&gt;Before pipes make sense, you need to understand where a command's output actually goes.&lt;/p&gt;

&lt;p&gt;Every normal process started from a shell usually begins with three standard streams open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FD&lt;/th&gt;
&lt;th&gt;Name&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&lt;/td&gt;
&lt;td&gt;stdin&lt;/td&gt;
&lt;td&gt;Input — what the process reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;stdout&lt;/td&gt;
&lt;td&gt;Output — normal results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;stderr&lt;/td&gt;
&lt;td&gt;Error output — diagnostic messages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you run &lt;code&gt;ls&lt;/code&gt;, the list of files goes to stdout (FD 1). If &lt;code&gt;ls&lt;/code&gt; can't find a directory, the error message goes to stderr (FD 2). Both land in your terminal by default, but they're separate streams.&lt;/p&gt;

&lt;p&gt;This distinction matters. Without it, you couldn't separate errors from results. You couldn't log errors while processing output. Every automation trick in the rest of this post depends on understanding that stdout and stderr are two different pipes.&lt;/p&gt;

&lt;p&gt;The numbers are not arbitrary. They're hardcoded in the POSIX standard — every Unix-like system since the 1970s uses 0, 1, and 2 for these three streams. On Linux, you can see them yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /proc/&lt;span class="nv"&gt;$$&lt;/span&gt;/fd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shows the file descriptors for your current shell process. You'll typically see something like &lt;code&gt;0 -&amp;amp;gt; /dev/pts/0&lt;/code&gt;, &lt;code&gt;1 -&amp;amp;gt; /dev/pts/0&lt;/code&gt;, &lt;code&gt;2 -&amp;amp;gt; /dev/pts/0&lt;/code&gt; — each pointing at your terminal device. This is why output and errors both appear on screen: they're sent to the same place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvv04s4d9boy89tk9qwb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcvv04s4d9boy89tk9qwb.webp" alt="Terminal showing file descriptors — ls -l /proc/$$/fd output with three numbered symlinks for stdin, stdout, and stderr pointing to the terminal device" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipe: One Command's Output, Another Command's Input
&lt;/h2&gt;

&lt;p&gt;The pipe is usually credited to Douglas McIlroy at Bell Labs, who proposed the idea in a 1964 memo. Ken Thompson implemented it in 1973 — "in one feverish night," McIlroy later wrote — adding the &lt;code&gt;pipe()&lt;/code&gt; system call and the &lt;code&gt;|&lt;/code&gt; syntax to Unix. The next day, McIlroy recalled, "saw an unforgettable orgy of one-liners as everybody joined in the excitement of plumbing."&lt;/p&gt;

&lt;p&gt;The pipe operator &lt;code&gt;|&lt;/code&gt; takes stdout from the left command and feeds it to stdin of the right command. No intermediate file. No extra temporary file on disk. Just a kernel buffer connecting two processes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Without pipes: two steps, a temp file&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; /tmp/files.txt
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt; /tmp/files.txt
&lt;span class="nb"&gt;rm&lt;/span&gt; /tmp/files.txt

&lt;span class="c"&gt;# With pipes: one step, no cleanup&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipes chain. The output of &lt;code&gt;wc -l&lt;/code&gt; can feed into another pipe, and another. There's no practical limit on a modern system — I've used chains of 12+ commands in log analysis scripts and they run just fine. Each stage in a pipeline runs as a separate process; shell built-ins in a pipeline often run in a subshell environment, so very long chains do create process overhead, but for the kind of automation you're doing in a terminal or script, you won't hit it.&lt;/p&gt;

&lt;p&gt;A pipe only carries stdout. Stderr still goes to your terminal. This is a feature — you can see error messages while data flows silently through the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redirection: The Six Operators
&lt;/h2&gt;

&lt;p&gt;Redirection sends output to files instead of the terminal, or reads input from files instead of the keyboard. Six operators cover everything you'll need.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;&amp;amp;gt;&lt;/code&gt; — Write stdout to a file (overwrite)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server online"&lt;/span&gt; &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; status.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creates the file if it doesn't exist. Overwrites it if it does. Use &lt;code&gt;set -o noclobber&lt;/code&gt; (or &lt;code&gt;set -C&lt;/code&gt;) in your shell to prevent accidental overwrites — then &lt;code&gt;&amp;amp;gt;|&lt;/code&gt; forces overwrite when you mean it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;&amp;amp;gt;&amp;amp;gt;&lt;/code&gt; — Append stdout to a file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"backup completed at &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; backup.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adds to the end. Doesn't touch existing content. This is the one you want for log files.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;&amp;amp;lt;&lt;/code&gt; — Read stdin from a file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt; access.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command reads the file as its stdin. Not the same as passing a filename as an argument — &lt;code&gt;wc -l access.log&lt;/code&gt; prints the filename alongside the count; &lt;code&gt;wc -l &amp;amp;lt; access.log&lt;/code&gt; only prints the number. This matters in pipelines where you want clean data, not filenames mixed in.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;2&amp;amp;gt;&lt;/code&gt; — Write stderr to a file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.conf"&lt;/span&gt; 2&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; errors.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only stderr goes to the file. Stdout still appears on your terminal. This is how you collect errors without losing your results.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;2&amp;amp;gt;&amp;amp;amp;1&lt;/code&gt; — Merge stderr into stdout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs my-app 2&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt;1 | &lt;span class="nb"&gt;grep &lt;/span&gt;error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read right to left: redirect FD 2 (stderr) to wherever FD 1 (stdout) is pointing. After this, both streams go to the same place. The order matters. &lt;code&gt;2&amp;amp;gt;&amp;amp;amp;1 &amp;amp;gt; file&lt;/code&gt; does NOT do what you think — it redirects stderr to whatever stdout was pointing at &lt;em&gt;when that operator was evaluated&lt;/em&gt; (your terminal), then redirects stdout to the file. You want &lt;code&gt;&amp;amp;gt; file 2&amp;amp;gt;&amp;amp;amp;1&lt;/code&gt; — redirect stdout first, then point stderr at the same destination. I still get this wrong sometimes and have to test it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;&amp;amp;amp;&amp;amp;gt;&lt;/code&gt; — Redirect both stdout and stderr to a file (Bash shorthand)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make all &amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; build.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Equivalent to &lt;code&gt;&amp;amp;gt; build.log 2&amp;amp;gt;&amp;amp;amp;1&lt;/code&gt;. Cleaner to type. Works in Bash and Zsh but not in plain &lt;code&gt;/bin/sh&lt;/code&gt; — if you're writing scripts for maximum portability, use the long form. In practice, most of us are in Bash anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  tee: Capturing Mid-Stream
&lt;/h2&gt;

&lt;p&gt;Sometimes you want to see output on screen AND write it to a file. That's &lt;code&gt;tee&lt;/code&gt; — named after the T-junction in plumbing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./long-script.sh | &lt;span class="nb"&gt;tee &lt;/span&gt;output.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script's output appears in your terminal and gets written to &lt;code&gt;output.log&lt;/code&gt; simultaneously. If you want to append instead of overwrite: &lt;code&gt;tee -a&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Where &lt;code&gt;tee&lt;/code&gt; really earns its place is in pipelines with &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"new config line"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/app/config.conf &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo echo &amp;amp;gt; file&lt;/code&gt; doesn't work because the redirection happens in your shell, before &lt;code&gt;sudo&lt;/code&gt; runs. &lt;code&gt;tee&lt;/code&gt; runs with elevated privileges and writes the file itself. This is one of those patterns you use once and then keep in your back pocket forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  xargs: When Pipes Aren't Enough
&lt;/h2&gt;

&lt;p&gt;A pipe feeds stdin. Not all commands read from stdin — many expect arguments. &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;docker stop&lt;/code&gt; — they take filenames or IDs as arguments, not on stdin.&lt;/p&gt;

&lt;p&gt;That's where &lt;code&gt;xargs&lt;/code&gt; bridges the gap. It reads stdin and turns each line into an argument for another command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find all .log files and delete them&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.log"&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0r&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt;

&lt;span class="c"&gt;# Stop all Docker containers whose image name contains 'old-'&lt;/span&gt;
docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.ID}} {{.Image}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'old-'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; | xargs &lt;span class="nt"&gt;-r&lt;/span&gt; docker stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;xargs&lt;/code&gt;, you'd need a &lt;code&gt;while read&lt;/code&gt; loop. With it, you get the same result in one clean pipeline.&lt;/p&gt;

&lt;p&gt;If filenames or arguments contain spaces, use &lt;code&gt;xargs -0&lt;/code&gt; with null-delimited input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.log"&lt;/span&gt; &lt;span class="nt"&gt;-print0&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-print0&lt;/code&gt; and &lt;code&gt;-0&lt;/code&gt; flags use the null character as a delimiter instead of newlines. Spaces in filenames won't break your pipeline. I learned this the hard way after deleting the wrong file in 2024 — now &lt;code&gt;-print0 | xargs -0&lt;/code&gt; is muscle memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process Substitution: When Two Inputs Collide
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; compares two files. But what if you want to compare the output of two commands — say, &lt;code&gt;ls&lt;/code&gt; from two different directories?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /dir1&lt;span class="o"&gt;)&lt;/span&gt; &amp;amp;lt&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /dir2&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;amp;lt;(command)&lt;/code&gt; runs the command in a subshell and presents its output as if it were a file. On many Linux systems, the &lt;code&gt;&amp;amp;lt;(...)&lt;/code&gt; gets replaced with a path like &lt;code&gt;/dev/fd/63&lt;/code&gt;, which &lt;code&gt;diff&lt;/code&gt; reads like any other file. Both commands in the example run in separate subshells, simultaneously.&lt;/p&gt;

&lt;p&gt;The reverse also works. &lt;code&gt;&amp;amp;gt;(command)&lt;/code&gt; lets you send output to a command as if it were a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tar &lt;/span&gt;czf &amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;ssh user@remote &lt;span class="s2"&gt;"cat &amp;amp;gt; backup.tgz"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; /home/mike/docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a tar archive and pipes it over SSH — no temporary file on disk. Not something you use daily, but when you need it, nothing else does the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Isn't in the CS Syllabus (And Why It Should Be)
&lt;/h2&gt;

&lt;p&gt;In my IGCSE classroom, we go beyond the syllabus — which covers OS interfaces in broad terms — to teach the CLI vs. GUI distinction. Students learn that a CLI exists. They learn it's faster for automation and preferred by technical users.&lt;/p&gt;

&lt;p&gt;What they don't learn is the mechanism. Classroom coverage stays at the &lt;em&gt;what&lt;/em&gt; — command line interfaces let you type commands — but stops short of the &lt;em&gt;how&lt;/em&gt;: the Unix philosophy of small, composable programs connected by pipes.&lt;/p&gt;

&lt;p&gt;This matters because pipes and redirection aren't just terminal tricks. They're the embodiment of a design principle that shows up everywhere in computing: composition over monoliths. Microservices pass data through APIs. React components pass props. &lt;a href="https://www.techiemike.com/how-to-set-up-a-test-pipeline-that-actually-catches-bugs/" rel="noopener noreferrer"&gt;CI/CD pipelines&lt;/a&gt; chain build steps. The pipe operator from 1973 is the same idea, at the operating system level.&lt;/p&gt;

&lt;p&gt;When a student understands pipes, they understand why &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;wc&lt;/code&gt;, and &lt;code&gt;head&lt;/code&gt; are separate programs instead of one giant "log-analyser." Each does one thing. Pipes let you combine them into whatever tool you need, on the fly. No configuration file. No GUI. Just composable building blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Automation: What This Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Theory is fine. Here's what pipes look like in my daily work running a homelab — the same patterns I use across the services &lt;a href="https://www.techiemike.com/proxmox-homelab-setup-on-a-mini-pc-ubuntu-vms-beyond/" rel="noopener noreferrer"&gt;running on my Proxmox mini PC&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finding what's eating disk space:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /&lt;span class="k"&gt;*&lt;/span&gt; 2&amp;amp;gt&lt;span class="p"&gt;;&lt;/span&gt;/dev/null | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rh&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checking Docker container health at a glance:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}} {{.Status}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'Up'&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/unhealthy.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extracting unique IPs from an nginx access log:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; /var/log/nginx/access.log | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Monitoring memory usage across services:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pmem,comm &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-pmem&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jpiby4aih7o0kc2gusy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1jpiby4aih7o0kc2gusy.webp" alt="Multi-command pipeline in a terminal — five commands chained with the pipe operator, filtering and sorting process output in real time" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each of these is a sentence. Subject, verb, object. The pipe is the verb — it connects, filters, sorts. Once you think in pipes, you stop reaching for Python scripts for one-off tasks. The terminal becomes fast enough that the answer is already on your screen by the time you'd have opened an editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unix Philosophy, in One Keystroke
&lt;/h2&gt;

&lt;p&gt;McIlroy's 1964 memo described a system where programs would be "coupl[ed]... like garden hose — screw in another segment when it becomes necessary to massage data in another way." He was describing the pipe, but he was also describing an approach to software that's outlasted every GUI framework and programming paradigm that's come along since.&lt;/p&gt;

&lt;p&gt;The next time you chain five commands with &lt;code&gt;|&lt;/code&gt; and get an answer in seconds, you're not just being efficient. You're using a design pattern from 1973 that modern distributed systems are still rediscovering.&lt;/p&gt;

&lt;p&gt;And if you're a CS student who just learned what a CLI is, know this: the CLI itself isn't what makes the terminal powerful. It's the pipes. It was always the pipes.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>tutorial</category>
      <category>bash</category>
      <category>commandline</category>
    </item>
    <item>
      <title>Homelab Network Design: Building Your Own Miniature Data Centre at Home</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Tue, 07 Jul 2026 02:01:14 +0000</pubDate>
      <link>https://dev.to/techiemike/homelab-network-design-building-your-own-miniature-data-centre-at-home-2oaj</link>
      <guid>https://dev.to/techiemike/homelab-network-design-building-your-own-miniature-data-centre-at-home-2oaj</guid>
      <description>&lt;h1&gt;
  
  
  Homelab Network Design: Building Your Own Miniature Data Centre at Home
&lt;/h1&gt;

&lt;p&gt;My homelab started flat. One subnet. Every device — the Proxmox host, the Docker containers, the media server, the WiFi light bulbs — all splashing around in the same 192.168.1.0/24 pool. It worked. Until it didn't.&lt;/p&gt;

&lt;p&gt;The moment things shifted was when I stood up a second physical node. I had the BMAX Pro 8 running Proxmox as my main workhorse, and the CHUWI MiniBook X joined the rack as a secondary compute node. Suddenly I had services that needed to talk to each other across machines, services that should never see each other, and a growing list of Docker networks that had no business being on the same broadcast domain as my workstation.&lt;/p&gt;

&lt;p&gt;Flat networks are the path of least resistance. And for a homelab running a single project, they're the correct design. Don't over-engineer before you need to. But once you cross the threshold with multiple physical nodes, tenant-like isolation between services, or anything internet-facing sharing a wire with your NAS, the flat design stops being simple and starts being a liability.&lt;/p&gt;

&lt;p&gt;This is the network design I landed on — why each decision matters, and the configs that hold it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Flat Network Trap
&lt;/h2&gt;

&lt;p&gt;Before the refactor, my layout looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ISP router (192.168.1.1)
  └── unmanaged switch
        ├── BMAX Pro 8 (Proxmox host, 192.168.1.50)
        │     ├── Docker containers (bridge network, 172.17.0.0/16)
        │     └── VMs (bridged, all on 192.168.1.0/24)
        ├── CHUWI MiniBook X (secondary, 192.168.1.51)
        ├── NAS (192.168.1.30)
        └── Workstation (192.168.1.10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything reachable from everything. Docker containers on the default bridge network. VMs bridged directly to the LAN. Broadcast traffic and service discovery noise had nowhere else to go.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foglu7h1j0e2u8w4ksfeh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foglu7h1j0e2u8w4ksfeh.webp" alt="Homelab flat network topology showing all devices on a single subnet with unmanaged switch" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That design can't answer three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you write different firewall rules for Home Assistant and Jellyfin when they share an IP range?&lt;/li&gt;
&lt;li&gt;If someone compromises a container, what stops them from scanning every device on your LAN?&lt;/li&gt;
&lt;li&gt;When something is saturating the network, which service is it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The operational tell is the last one. When "what's chatty on the wire right now?" becomes a question you can't answer from a switchport counter, the design has outgrown its problem.&lt;/p&gt;

&lt;p&gt;Enterprise networks solved this decades ago with segmentation. The same patterns scale down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think Miniature Data Centre, Not Home Network
&lt;/h2&gt;

&lt;p&gt;Commercial data centres isolate workloads into dedicated network segments. Each gets its own VLAN, its own subnet, its own firewall posture, its own blast radius. When something goes wrong in one segment, the fault is bounded. When a service needs a different security posture, it gets one without dragging the rest of the rack along.&lt;/p&gt;

&lt;p&gt;That's the model. Scaled down to a homelab, it looks like this:&lt;/p&gt;

&lt;p&gt;Every project gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own VLAN&lt;/li&gt;
&lt;li&gt;Its own subnet&lt;/li&gt;
&lt;li&gt;Explicit firewall rules at the routing boundary&lt;/li&gt;
&lt;li&gt;No default trust with any other VLAN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hypervisor stays on a management network. Workloads live on their own segments. Ops access is explicit. No implicit reachability just because you're on the same switch.&lt;/p&gt;

&lt;p&gt;This isn't over-engineering. It's the difference between a network that tolerates growth and one that needs to be torn down and rebuilt every time you add a service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Physical Layer
&lt;/h2&gt;

&lt;p&gt;The physical topology constrains the logical design. Here's what's actually plugged in:&lt;/p&gt;

&lt;p&gt;My setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BMAX Pro 8 (i7-1260P)&lt;/td&gt;
&lt;td&gt;Primary Proxmox host&lt;/td&gt;
&lt;td&gt;2× 2.5Gb Ethernet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHUWI MiniBook X (N150)&lt;/td&gt;
&lt;td&gt;Secondary node / portable&lt;/td&gt;
&lt;td&gt;1× USB-C Ethernet dongle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TP-Link TL-SG108E&lt;/td&gt;
&lt;td&gt;Managed switch (8-port, VLAN-capable)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISP Router&lt;/td&gt;
&lt;td&gt;DHCP + internet gateway&lt;/td&gt;
&lt;td&gt;1× 1Gb&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogfb2tuab0gce7i6h3cq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fogfb2tuab0gce7i6h3cq.webp" alt="Physical homelab setup: two mini PCs, managed switch, and ISP router on a shelf" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key piece is the managed switch. An unmanaged switch cannot create or manage VLAN membership. Some may pass tagged frames through, but they cannot assign access ports, define trunks, or enforce segmentation. If you want network segmentation, you need a switch that understands &lt;a href="https://standards.ieee.org/ieee/802.1Q/6844/" rel="noopener noreferrer"&gt;802.1Q VLAN tagging&lt;/a&gt;. You don't need a Cisco Nexus. An entry-level TP-Link or Netgear managed switch with VLAN support is enough to get started. I use the &lt;a href="https://www.techiemike.com/chuwi-minibook-x-vs-bmax-pro-8-which-budget-homelab-machine/" rel="noopener noreferrer"&gt;BMAX Pro 8&lt;/a&gt; for most services and the CHUWI MiniBook X as a secondary Proxmox node for lighter workloads.&lt;/p&gt;

&lt;p&gt;The ISP router stays as the internet gateway but does not handle inter-VLAN routing. A router, firewall VM, or L3 switch has to do that job. With an entry-level L2 managed switch like the TL-SG108E, VLANs give you separation, but routing between them still needs something like OPNsense, pfSense, or a router that supports VLAN interfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  VLANs and Network Segmentation
&lt;/h2&gt;

&lt;p&gt;The logical layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VLAN 1  (Management):  192.168.1.0/24    — Proxmox host, switch management, workstation
VLAN 10 (Services):    10.0.10.0/24      — Docker hosts, web services, reverse proxy
VLAN 20 (Media):       10.0.20.0/24      — Jellyfin, NAS, media-related containers
VLAN 30 (IoT):         10.0.30.0/24      — Home Assistant, ESPHome devices, Zigbee bridge
VLAN 40 (Lab):         10.0.40.0/24      — Throwaway VMs, pen-test targets, experiments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact VLAN numbers do not matter — consistency and documentation matter more than copying someone else's numbering scheme.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe965nzititf347f3bk4v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe965nzititf347f3bk4v.webp" alt="Homelab VLAN topology showing four network segments with subnet assignments and firewall routing boundaries" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three design rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hypervisor stays on management, VMs live on workload VLANs.&lt;/strong&gt; The Proxmox host isn't a workload. It's the platform. If a workload VLAN's routing breaks, you still need console access to the host for recovery. Keeping it on the management VLAN means backups continue, snapshots run, and you can SSH in regardless of what's happening on VLAN 10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One subnet per function, gateway at the switch or firewall.&lt;/strong&gt; The routing boundary between VLANs is where policy lives. Every packet crossing from VLAN 10 to VLAN 30 hits the gateway, and that's where you write the rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No VLAN trusts another by default.&lt;/strong&gt; Inter-VLAN traffic is denied. ACLs grant the exceptions. If Jellyfin on VLAN 20 needs to reach the NAS, that's a deliberate &lt;code&gt;permit&lt;/code&gt; rule, not a side effect of sharing a broadcast domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  VLAN Configuration on a Managed Switch
&lt;/h3&gt;

&lt;p&gt;On a TP-Link TL-SG108E (or similar entry-level managed switch):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VLAN ID: 10
Name: Services
Port Members: 1 (tagged), 3 (untagged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 1 is the trunk to the Proxmox host, tagged so it carries multiple VLANs. Port 3 might be a physical device that only speaks untagged traffic.&lt;/p&gt;

&lt;p&gt;The switch's 802.1Q config page is where you map VLAN IDs to ports. Tagged ports carry traffic for multiple VLANs (each frame gets a VLAN tag), untagged ports belong to a single VLAN. The Proxmox trunk port should carry every VLAN your VMs need. Tag them all and let Proxmox sort out which VM gets which.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxmox Networking
&lt;/h2&gt;

&lt;p&gt;Proxmox bridges are the glue between the physical switch VLANs and your VMs. The config lives in &lt;code&gt;/etc/network/interfaces&lt;/code&gt;. If you haven't set up Proxmox yet, start with the &lt;a href="https://www.techiemike.com/proxmox-homelab-setup-on-a-mini-pc-ubuntu-vms-beyond/" rel="noopener noreferrer"&gt;Proxmox Homelab Setup on a Mini PC&lt;/a&gt; guide first — it covers the base install before you add VLANs.&lt;/p&gt;

&lt;p&gt;A VLAN-aware bridge setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auto vmbr0
iface vmbr0 inet static
    address 192.168.1.50/24
    gateway 192.168.1.1
    bridge-ports enp1s0
    bridge-stp off
    bridge-fd 0
    bridge-vlan-aware yes
    bridge-vids 2-4094
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bridge-vlan-aware yes&lt;/code&gt; is the important flag. Without VLAN awareness, Proxmox will not handle multiple VLAN-tagged guest interfaces cleanly on that bridge. You can still build VLAN setups in other ways, but a VLAN-aware bridge is the cleanest Proxmox approach when one trunk carries several VM VLANs.&lt;/p&gt;

&lt;p&gt;When creating a VM, set the network interface VLAN tag to 10, and that VM lands in the Services VLAN. No DHCP server on VLAN 10? Assign a static IP or run a DHCP server inside that VLAN: a container running dnsmasq, or your router if it supports multiple DHCP scopes.&lt;/p&gt;

&lt;p&gt;For containers (LXC), the network config in &lt;code&gt;/etc/pve/lxc/.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=AA:BB:CC:DD:EE:FF,tag=10,type=veth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tag=10&lt;/code&gt; puts the container on VLAN 10. Adding &lt;code&gt;firewall=1&lt;/code&gt; enables the Proxmox firewall for that interface, useful as a second layer of filtering before traffic even hits the switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Management Network Rule
&lt;/h3&gt;

&lt;p&gt;Proxmox's web UI and SSH live on the management bridge (VLAN 1, untagged). Never put the management interface on a workload VLAN. If you lose routing to VLAN 10, you don't want to lose access to the host. The management network should stay reachable even when every workload VLAN is on fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker Networking
&lt;/h2&gt;

&lt;p&gt;Docker adds a third networking layer on top of the physical switch and Proxmox bridges. Containers on the same Docker network can reach each other by service name. Containers on different Docker networks are isolated unless you explicitly connect them.&lt;/p&gt;

&lt;p&gt;The default bridge network (&lt;code&gt;docker0&lt;/code&gt;, 172.17.0.0/16) is fine for a single-container experiment. For anything that runs multiple services, create dedicated networks. (New to Docker? &lt;a href="https://www.techiemike.com/learning-docker-by-building-a-container-engine-from-scratch/" rel="noopener noreferrer"&gt;Learning Docker by Building a Container Engine From Scratch&lt;/a&gt; walks through the fundamentals.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;ipam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;subnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;172.20.0.0/24&lt;/span&gt;
  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;ipam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;subnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;172.21.0.0/24&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;app&lt;/code&gt; container sits on both networks. It can talk to the database on &lt;code&gt;backend&lt;/code&gt; and receive traffic from the reverse proxy on &lt;code&gt;frontend&lt;/code&gt;. The database is on &lt;code&gt;backend&lt;/code&gt; only, with no direct exposure to the proxy network. This is a miniature three-tier web architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Docker Networks Map to VLANs
&lt;/h3&gt;

&lt;p&gt;Docker networks live inside the Proxmox host. They don't automatically map to physical VLANs. A container on a Docker bridge network can talk to other containers on the same bridge, but it can't reach a VM on VLAN 20 without a route.&lt;/p&gt;

&lt;p&gt;For containers that need physical VLAN access, use the &lt;code&gt;macvlan&lt;/code&gt; driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;media-vlan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;macvlan&lt;/span&gt;
    &lt;span class="na"&gt;driver_opts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eth0.20&lt;/span&gt;
    &lt;span class="na"&gt;ipam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;subnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.20.0/24&lt;/span&gt;
          &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.20.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;parent&lt;/code&gt; value must match the interface visible inside the Docker host, not necessarily the Proxmox physical NIC. On a Docker VM this might be &lt;code&gt;eth0&lt;/code&gt;; on a bare-metal Docker host it might be &lt;code&gt;enp1s0.20&lt;/code&gt;; on a Proxmox-hosted setup it depends on whether VLAN tagging is handled by Proxmox, the guest, or a dedicated bridge. The warning that macvlan containers often cannot communicate directly with the Docker host is correct — Docker's macvlan behaviour commonly isolates host/container communication by design.&lt;/p&gt;

&lt;p&gt;The trade-off: macvlan containers bypass the host's network stack, so the host itself can't communicate with them on that interface. For most homelab services, stick with bridge networks and use the reverse proxy as the routing layer between Docker and the rest of the network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inspect Docker networks&lt;/span&gt;
docker network &lt;span class="nb"&gt;ls
&lt;/span&gt;docker network inspect backend

&lt;span class="c"&gt;# See which containers are on which networks&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For networks that should never reach the internet, Docker's &lt;code&gt;internal: true&lt;/code&gt; flag creates an externally isolated network — containers on that network cannot use Docker's normal bridge path to reach external networks. Databases and internal services that only talk to other containers are good candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse Proxy: The Front Door
&lt;/h2&gt;

&lt;p&gt;Every service that needs external access routes through a reverse proxy. I use &lt;a href="https://caddyserver.com/docs/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; because it handles TLS certificates automatically and the config is five lines. nginx and Traefik are equally solid choices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Caddyfile
jellyfin.techie.local {
    reverse_proxy 10.0.20.10:8096
}

homeassistant.techie.local {
    reverse_proxy 10.0.30.12:8123
}

portainer.techie.local {
    reverse_proxy 172.20.0.5:9443
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse proxy sits on the Services VLAN with an interface in the Docker bridge network. From there it reaches Jellyfin on VLAN 20 (via the gateway's inter-VLAN routing), Home Assistant on VLAN 30, and Portainer inside Docker, all through a single TLS-terminated entry point.&lt;/p&gt;

&lt;p&gt;Access control happens at the proxy layer. If a service doesn't need internet exposure, don't put it in the Caddyfile. Internal services like databases stay on isolated Docker networks and are reachable only by the containers that need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting a Let's Encrypt Certificate for Internal Services
&lt;/h3&gt;

&lt;p&gt;Caddy handles this automatically for public domains. For internal-only services (&lt;code&gt;.local&lt;/code&gt; or &lt;code&gt;.home.arpa&lt;/code&gt; domains), use a DNS challenge with your domain provider's API token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jellyfin.internal.techie.systems {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy 10.0.20.10:8096
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets you a valid certificate for an internal IP without opening port 80 to the internet. The DNS challenge proves you control the domain, and Caddy handles renewal.&lt;/p&gt;

&lt;p&gt;Note: the Cloudflare DNS provider is not included in the default Caddy build. Install a Caddy build that includes &lt;code&gt;dns.providers.cloudflare&lt;/code&gt;, or build it with &lt;code&gt;xcaddy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Firewall Rules Between Segments
&lt;/h2&gt;

&lt;p&gt;VLAN segmentation isolates broadcast domains. It doesn't block traffic by default. Two VLANs on the same switch, both trunked to the same Proxmox host, can absolutely talk to each other as soon as there's a route between them. Firewall rules at the routing boundary are what enforce isolation.&lt;/p&gt;

&lt;p&gt;On a router or firewall VM (OPNsense, pfSense, or even iptables on the Proxmox host), the rule pattern for a workload VLAN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Services VLAN (10.0.10.0/24) rules
allow from 10.0.10.0/24 to 10.0.20.10:8096    # Jellyfin access
allow from 192.168.1.10/32 to 10.0.10.0/24    # Workstation management
block from 10.0.10.0/24 to 10.0.0.0/8         # Block all other inter-VLAN
allow from 10.0.10.0/24 to any                 # Internet outbound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule order matters. The default deny catches everything not explicitly permitted. Internet outbound is the last rule. Everything else is inter-VLAN and gets evaluated first.&lt;/p&gt;

&lt;p&gt;For Docker containers with iptables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow a container to reach Jellyfin on the Media VLAN&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.5 &lt;span class="nt"&gt;-d&lt;/span&gt; 10.0.20.10 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 8096 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Block that container from reaching anything else&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.5 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;DOCKER-USER&lt;/code&gt; chain is the right place for custom Docker firewall rules. Docker inserts its own rules in the &lt;code&gt;DOCKER&lt;/code&gt; chain, and &lt;code&gt;DOCKER-USER&lt;/code&gt; is evaluated first.&lt;/p&gt;

&lt;p&gt;Be careful with broad DROP rules in &lt;code&gt;DOCKER-USER&lt;/code&gt;. Add required DNS, gateway, update-server, and established-connection rules first, or you may cut the container off from more than intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands That Actually Tell You What's Happening
&lt;/h2&gt;

&lt;p&gt;Theory is fine. These are the commands I run when I'm debugging the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What networks exist on this Docker host?&lt;/span&gt;
docker network &lt;span class="nb"&gt;ls&lt;/span&gt;

&lt;span class="c"&gt;# Which containers are on which network?&lt;/span&gt;
docker network inspect backend | jq &lt;span class="s1"&gt;'.[].Containers | to_entries[] | {name: .value.Name, ip: .value.IPv4Address}'&lt;/span&gt;

&lt;span class="c"&gt;# What routes does this machine have?&lt;/span&gt;
ip route show

&lt;span class="c"&gt;# Which VLAN-tagged interfaces exist?&lt;/span&gt;
ip &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nb"&gt;link &lt;/span&gt;show | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; vlan

&lt;span class="c"&gt;# What's listening on which ports?&lt;/span&gt;
ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt;

&lt;span class="c"&gt;# Trace the route a packet takes to a service&lt;/span&gt;
traceroute 10.0.20.10

&lt;span class="c"&gt;# Is the firewall blocking this?&lt;/span&gt;
iptables &lt;span class="nt"&gt;-L&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;docker network inspect&lt;/code&gt; command is the one I reach for most. It shows you exactly which containers are on a network, their IPs, and their MAC addresses. Invaluable when you're trying to figure out why service A can't reach service B.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;I built this incrementally. If I were starting from scratch, I'd do four things differently:&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with a Managed Switch
&lt;/h3&gt;

&lt;p&gt;I spent months on an unmanaged switch, working around the lack of VLANs with separate physical interfaces and USB Ethernet dongles. A managed switch would have saved me a dozen evenings of network gymnastics.&lt;/p&gt;

&lt;h3&gt;
  
  
  One VLAN per Function, Not per Project
&lt;/h3&gt;

&lt;p&gt;I initially created a VLAN per application (Jellyfin VLAN, Home Assistant VLAN, etc.). That's too granular for a homelab. Three or four functional VLANs (management, services, media, lab) cover everything without turning your switch config into a part-time job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document the IP Scheme Before You Need It
&lt;/h3&gt;

&lt;p&gt;When you're troubleshooting at midnight, a text file with every static IP, VLAN assignment, and port mapping is worth its weight in sleep. I keep mine in Obsidian, but a markdown file in the repo works fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't VLAN-Tag Your Workstation Port
&lt;/h3&gt;

&lt;p&gt;Tag it wrong and you lose access to the switch management interface. Keep one untagged management port as a recovery path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CS Angle — Why This Matters for Students
&lt;/h2&gt;

&lt;p&gt;If you're studying Cambridge IGCSE or A-Level Computer Science, a homelab network is the most practical way to internalise the networking syllabus. Subnetting, network segmentation, and subnetwork models are in the A-Level spec (9618) — and VLANs are how those concepts play out on real hardware. Subnet masks, CIDR notation, the role of routers and switches — it's all there.&lt;/p&gt;

&lt;p&gt;But textbooks teach these concepts in isolation. A VLAN is a paragraph. A subnet mask is a slide. A homelab makes them real: you type &lt;code&gt;ip route show&lt;/code&gt;, you see the routing table. You configure a VLAN on a switch, you watch tagged frames hit the port. You misconfigure a subnet mask and suddenly two devices can't talk, and you understand &lt;em&gt;why&lt;/em&gt; in a way no exam question can replicate.&lt;/p&gt;

&lt;p&gt;Even the security concepts click differently. "Defence in depth" is an abstract principle until you're writing iptables rules that block a compromised container from pivoting to your NAS. Then it's not theory anymore. It's your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started — The Quick-Start Template
&lt;/h2&gt;

&lt;p&gt;A minimal setup you can stand up in an afternoon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Buy a managed switch.&lt;/strong&gt; TP-Link TL-SG108E or similar entry-level managed switch. 8 ports, VLAN support, web-managed. Plug it in between your ISP router and your Proxmox host.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create three VLANs on the switch.&lt;/strong&gt; Management (VLAN 1, untagged), Services (VLAN 10, tagged), Lab (VLAN 40, tagged). Tag the port that connects to your Proxmox host. Before changing anything, make sure you have one untagged management port left as a recovery path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable VLAN-aware bridge on Proxmox.&lt;/strong&gt; Add &lt;code&gt;bridge-vlan-aware yes&lt;/code&gt; to &lt;code&gt;/etc/network/interfaces&lt;/code&gt; on your bridge. Assign VM network interfaces to VLAN 10 or 40 with the &lt;code&gt;tag&lt;/code&gt; parameter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create dedicated Docker networks.&lt;/strong&gt; Stop using the default bridge. Define &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt; networks in your Docker Compose files. Put databases on &lt;code&gt;backend&lt;/code&gt; only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set up a reverse proxy.&lt;/strong&gt; Install Caddy or nginx on a VM or container in the Services VLAN. Add proxy entries for the services you want to access externally.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the skeleton. Everything else (firewall rules, inter-VLAN routing, macvlan for specific use cases) you add as the homelab grows.&lt;/p&gt;

&lt;p&gt;Start flat. But when the flat network stops working (and it will), you'll know which pieces to reach for.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>networking</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Self-Hosted AI Security: What Every Ollama User Should Know</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:01:12 +0000</pubDate>
      <link>https://dev.to/techiemike/self-hosted-ai-security-what-every-ollama-user-should-know-29ie</link>
      <guid>https://dev.to/techiemike/self-hosted-ai-security-what-every-ollama-user-should-know-29ie</guid>
      <description>&lt;h1&gt;
  
  
  Self-Hosted AI Security: What Every Ollama User Should Know
&lt;/h1&gt;

&lt;p&gt;Last week, a researcher publicly disclosed five Ollama and LiteLLM issues after a 90-day responsible disclosure window: two affecting Ollama directly and three affecting LiteLLM.&lt;/p&gt;

&lt;p&gt;That is not the same as "instant remote code execution on every homelab Ollama server", but it is still a warning sign. Self-hosted AI tools are now being researched, fingerprinted, and probed like any other internet-facing service.&lt;/p&gt;

&lt;p&gt;For most self-hosted AI setups, these are the kind of bugs that sit in the background until someone actually exploits them. And the thing about homelabs is that "someone" doesn't have to be a nation-state. It can be a scanner bot that found port 11434 on Shodan.&lt;/p&gt;

&lt;p&gt;I've been running &lt;a href="https://www.techiemike.com/self-hosted-llms-with-docker-ollama-and-open-webui/" rel="noopener noreferrer"&gt;Docker, Ollama, and Open WebUI&lt;/a&gt; on my homelab for months. The setup works well: local models, no API bills, no rate limits. But security was an afterthought when I first set it up, and I suspect that's true for most people. I've tightened up my own setup since reading the disclosures, and there are five things every Ollama user should do right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What got disclosed
&lt;/h2&gt;

&lt;p&gt;The recent disclosure covered five Ollama and LiteLLM issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama GGUF string length panic&lt;/strong&gt; — a crafted model file could trigger a crash, causing denial of service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama unbounded &lt;code&gt;vocab_size&lt;/code&gt; resource exhaustion&lt;/strong&gt; — a malicious model could consume excessive CPU or memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM pass-the-hash authentication bypass&lt;/strong&gt; — a weakness in authentication handling could allow access under specific conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM SSRF through custom guardrails&lt;/strong&gt; — an attacker could make the server reach internal or unintended services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM Unicode normalization issue&lt;/strong&gt; — a parsing or sandboxing weakness that could contribute to sandbox escape scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these should be described as "instant remote code execution on every homelab Ollama server". The practical lesson is simpler: if your AI stack is exposed without authentication, every parser bug, model-loading bug, and proxy bug becomes much more dangerous.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/04-images%2Fself-hosted-ai-security-ollama%2Fbody-01-attack-surface.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/04-images%2Fself-hosted-ai-security-ollama%2Fbody-01-attack-surface.webp" alt="Diagram showing the Ollama and LiteLLM vulnerability attack surface: Docker containers, exposed API ports, and network attack vectors" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ollama has also had other recent security issues. &lt;a href="https://www.sentinelone.com/vulnerability-database/cve-2026-5530/" rel="noopener noreferrer"&gt;CVE-2026-5530&lt;/a&gt; (&lt;a href="https://github.com/advisories/GHSA-r4wp-gg33-whwg" rel="noopener noreferrer"&gt;GitHub advisory&lt;/a&gt;) is reported as an SSRF flaw in the Model Pull API, and &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-7482" rel="noopener noreferrer"&gt;CVE-2026-7482&lt;/a&gt; (&lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-7482" rel="noopener noreferrer"&gt;CVE.org&lt;/a&gt;) is a heap out-of-bounds read in the GGUF model loader that could disclose server process memory. Those are separate from the five-issue Ollama/LiteLLM disclosure, but they reinforce the same point: exposed local AI services are now part of the attack surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your homelab is exposed
&lt;/h2&gt;

&lt;p&gt;The Ollama service itself is designed as a local API, but Docker port publishing can easily expose it. In Compose, &lt;code&gt;11434:11434&lt;/code&gt; publishes the container port on all host interfaces, which means LAN access — and possibly internet access if your firewall or router allows it.&lt;/p&gt;

&lt;p&gt;If you followed a typical setup guide, you probably have this in your &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11434:11434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;"11434:11434"&lt;/code&gt; is the problem. It binds to &lt;code&gt;0.0.0.0:11434&lt;/code&gt; on the host. Every device on your network can reach your Ollama API. If you've forwarded ports on your router (which most homelab users do for services like Plex or Nextcloud), then anyone on the internet can reach it too.&lt;/p&gt;

&lt;p&gt;I checked my own setup while researching this post. I had the Docker Compose configuration from my &lt;a href="https://www.techiemike.com/self-hosted-llms-with-docker-ollama-and-open-webui/" rel="noopener noreferrer"&gt;Ollama guide&lt;/a&gt; with &lt;code&gt;127.0.0.1:11434:11434&lt;/code&gt; (binding to localhost only) in the standalone &lt;code&gt;docker run&lt;/code&gt; command, but the Compose version had the default &lt;code&gt;11434:11434&lt;/code&gt;. I'd fixed it in one place and not the other. This is exactly the kind of inconsistency that creates security gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things to do today
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bind Ollama to localhost
&lt;/h3&gt;

&lt;p&gt;The simplest fix, and the one that eliminates the largest attack surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:11434:11434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;127.0.0.1:&lt;/code&gt; prefix means Ollama only listens on localhost. Services running on the same machine can still reach it: Open WebUI, n8n, anything that talks to &lt;code&gt;http://ollama:11434&lt;/code&gt; inside the Docker network. But nothing outside the host can touch it.&lt;/p&gt;

&lt;p&gt;If you're using the standalone &lt;code&gt;docker run&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; ollama &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:11434:11434 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ollama_data:/root/.ollama &lt;span class="se"&gt;\&lt;/span&gt;
  ollama/ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should show 127.0.0.1:11434, not 0.0.0.0:11434&lt;/span&gt;
docker port ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/04-images%2Fself-hosted-ai-security-ollama%2Fbody-02-localhost-binding.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/04-images%2Fself-hosted-ai-security-ollama%2Fbody-02-localhost-binding.webp" alt="Comparison diagram: 0.0.0.0 binding exposes Ollama to the network vs 127.0.0.1 localhost-only binding" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put a reverse proxy with authentication in front
&lt;/h3&gt;

&lt;p&gt;Binding to localhost is good, but if you want to access Ollama from other devices on your network or remotely, you need a middle layer. A reverse proxy with authentication (Nginx, Caddy, or Traefik) sits between the outside world and Ollama.&lt;/p&gt;

&lt;p&gt;The simplest approach: if you already use Cloudflare for your domain (I do for techiemike.com), add &lt;a href="https://developers.cloudflare.com/cloudflare-one/applications/" rel="noopener noreferrer"&gt;Cloudflare Access&lt;/a&gt; in front of your Ollama endpoint. It's &lt;a href="https://www.cloudflare.com/plans/" rel="noopener noreferrer"&gt;free for up to 50 users&lt;/a&gt;, adds an email-based authentication gate, and you can set it up in about ten minutes. (I wrote about &lt;a href="https://www.techiemike.com/cloudflare-self-managed-oauth-homelab-security/" rel="noopener noreferrer"&gt;Cloudflare's new self-managed OAuth&lt;/a&gt; recently — it's worth reading if you're building anything that talks to the Cloudflare API from your homelab.)&lt;/p&gt;

&lt;p&gt;If you want a self-hosted auth solution, &lt;a href="https://goauthentik.io/" rel="noopener noreferrer"&gt;Authentik&lt;/a&gt; or &lt;a href="https://www.authelia.com/" rel="noopener noreferrer"&gt;Authelia&lt;/a&gt; work well with Nginx. Here's the Nginx config pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;ollama.yourdomain.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;auth_request&lt;/span&gt; &lt;span class="n"&gt;/auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:11434&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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 class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:9091/api/verify&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# Authentik&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass_request_body&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Content-Length&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even basic auth (a username and password) is better than an open API endpoint. But Cloudflare Access or Authentik with SSO is the proper solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Isolate Ollama on its own Docker network
&lt;/h3&gt;

&lt;p&gt;Ollama doesn't need to be on the default bridge network with all your other containers. Put it on a dedicated network so that even if someone compromises it, they can't pivot to your other services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# No outbound internet from this network&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ai_network&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;

  &lt;span class="na"&gt;open-webui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ai_network&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;proxy_network&lt;/span&gt;  &lt;span class="c1"&gt;# Only Open WebUI can reach the reverse proxy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;internal: true&lt;/code&gt; flag on the AI network means containers on it can talk to each other but can't reach the internet. Ollama doesn't need internet access after you've pulled your models. If someone exploits an SSRF vulnerability, the network layer stops the request before it leaves the Docker host.&lt;/p&gt;

&lt;p&gt;The trade-off: you can't pull new models while the network is internal. When you need to pull a model, temporarily switch &lt;code&gt;internal&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;, pull the model, then switch it back. It's a minor inconvenience for a significant security gain.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Run Ollama as a non-root user
&lt;/h3&gt;

&lt;p&gt;The official Ollama Docker image runs as root by default. If someone finds a container escape vulnerability, they land as root on your host. Running as a non-root user limits the damage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000:1000"&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:11434:11434"&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama_data:/home/user/.ollama&lt;/span&gt;  &lt;span class="c1"&gt;# Non-root home directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need to adjust the volume path since the default &lt;code&gt;/root/.ollama&lt;/code&gt; won't be writable by user 1000. Ollama supports the &lt;code&gt;OLLAMA_MODELS&lt;/code&gt; environment variable for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000:1000"&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;OLLAMA_MODELS=/home/user/.ollama/models&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:11434:11434"&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama_data:/home/user/.ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on a single-user homelab where you're the only person with SSH access, the risk of container escape is low. But it costs a few extra lines of config and follows a security principle that every CS student should learn: least privilege. Don't give a process more access than it needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Monitor API usage and set rate limits
&lt;/h3&gt;

&lt;p&gt;You can't secure what you can't see. Ollama doesn't have built-in access logging beyond stdout, but you can add it at the reverse proxy layer.&lt;/p&gt;

&lt;p&gt;With Nginx, add a log format that captures the request body size, user agent, and response time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;ollama_log&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt; &lt;span class="s"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$remote_user&lt;/span&gt; &lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$time_local&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                      &lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="nv"&gt;$body_bytes_sent&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                      &lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="nv"&gt;$http_user_agent&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$request_time&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/ollama_access.log&lt;/span&gt; &lt;span class="s"&gt;ollama_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set up rate limiting. No legitimate user needs to fire hundreds of requests per second at a local model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=ollama_limit:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=ollama_limit&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;# ... proxy config&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For homelab use, 10 requests per second with a burst of 20 is generous. You can tighten it if you're the only user.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this teaches about security
&lt;/h2&gt;

&lt;p&gt;Every one of these issues follows a pattern that shows up in CS syllabi worldwide. And every one of them was preventable with basic security hygiene.&lt;/p&gt;

&lt;p&gt;The SSRF vulnerability exists because the code trusts user input (a URL in a guardrail config) and requests whatever it points to. The Ollama GGUF parsing bugs exist because the code doesn't validate model file headers before processing them. The auth bypass exists because the middleware checks one header but the application logic reads another.&lt;/p&gt;

&lt;p&gt;These aren't exotic zero-days. They're the same class of bug that's been appearing in web applications for twenty years. What's new is the context: self-hosted AI tools are being deployed by people who aren't security engineers, in environments that weren't designed for the threat model they're now facing.&lt;/p&gt;

&lt;p&gt;If you're a CS student reading this (and a chunk of my readers are), these disclosures are a better education than any textbook. Look up the actual reports on Huntr. Read the patch diffs on the &lt;a href="https://github.com/ollama/ollama" rel="noopener noreferrer"&gt;Ollama GitHub&lt;/a&gt;. You'll learn more about real-world security from one vulnerability analysis than from a semester of theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;Ollama isn't uniquely dangerous. It's the same story we've seen with every self-hosted service: people deploy it, it works, and security gets deferred. I did it myself. My &lt;a href="https://www.techiemike.com/self-hosted-llms-with-docker-ollama-and-open-webui/" rel="noopener noreferrer"&gt;Docker + Ollama setup guide&lt;/a&gt; has been one of the most popular posts on this site, and I originally wrote it with the &lt;code&gt;127.0.0.1&lt;/code&gt; binding in one code block but not the Compose version. I only noticed the gap while researching this post.&lt;/p&gt;

&lt;p&gt;The difference with self-hosted AI is the data. Your conversations with a local model might include code from private repos, personal documents you're summarising, or API keys you're debugging. That data lives in your request logs, your model outputs, and potentially in whatever an attacker exfiltrates. It's not just compute you're protecting. It's context.&lt;/p&gt;

&lt;p&gt;I wrote recently about &lt;a href="https://www.techiemike.com/right-to-local-intelligence/" rel="noopener noreferrer"&gt;why running AI on your own hardware matters&lt;/a&gt;: the privacy, the cost, the independence from cloud platforms. All of that is still true. But independence comes with responsibility. When you're the cloud, you're also the security team.&lt;/p&gt;

&lt;p&gt;The five steps above take about twenty minutes to implement. They close the most common attack vectors without breaking anything. Do them today, before someone else's scanner bot finds your Ollama instance before you do.&lt;/p&gt;

</description>
      <category>security</category>
      <category>docker</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
