<?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: LK Wood IV</title>
    <description>The latest articles on DEV Community by LK Wood IV (@techfuelhq).</description>
    <link>https://dev.to/techfuelhq</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%2F4014612%2F6b8d19db-7873-49d5-9743-2335021740ac.jpg</url>
      <title>DEV Community: LK Wood IV</title>
      <link>https://dev.to/techfuelhq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techfuelhq"/>
    <language>en</language>
    <item>
      <title>Docker Exit Code 137: Prove OOMKilled Before Raising Memory</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:57:30 +0000</pubDate>
      <link>https://dev.to/techfuelhq/docker-exit-code-137-prove-oomkilled-before-raising-memory-d2b</link>
      <guid>https://dev.to/techfuelhq/docker-exit-code-137-prove-oomkilled-before-raising-memory-d2b</guid>
      <description>&lt;p&gt;Adapted from &lt;a href="https://techfuelhq.com/tutorials/docker-exit-code-137-oomkilled-2026/?utm_source=dev-to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=hardware-oct-2026&amp;amp;utm_content=docker-exit-137" rel="noopener noreferrer"&gt;Docker Exit Code 137: Prove OOMKilled Before Raising Memory&lt;/a&gt;, published on TechFuelHQ.&lt;/p&gt;

&lt;p&gt;A container has stopped with exit code &lt;code&gt;137&lt;/code&gt;. Before restarting it or increasing its memory limit, capture its identity, state and failure window. Those records help distinguish a container memory limit from host pressure, a stop timeout, or an external kill.&lt;/p&gt;

&lt;p&gt;The number alone cannot tell you which happened. &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html" rel="noopener noreferrer"&gt;Bash reports fatal signals as 128 + the signal number&lt;/a&gt;, and Linux &lt;a href="https://man7.org/linux/man-pages/man7/signal.7.html" rel="noopener noreferrer"&gt;assigns SIGKILL number 9&lt;/a&gt;. But &lt;a href="https://docs.docker.com/engine/containers/run/#exit-status" rel="noopener noreferrer"&gt;Docker returns the container command's exit status&lt;/a&gt;, so the command can return &lt;code&gt;137&lt;/code&gt; itself. Docker also documents two non-memory routes to SIGKILL: &lt;a href="https://docs.docker.com/reference/cli/docker/container/kill/" rel="noopener noreferrer"&gt;&lt;code&gt;docker kill&lt;/code&gt; sends it by default&lt;/a&gt;, while &lt;a href="https://docs.docker.com/reference/cli/docker/container/stop/" rel="noopener noreferrer"&gt;&lt;code&gt;docker stop&lt;/code&gt; escalates to it after the timeout&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture one container and one time window
&lt;/h2&gt;

&lt;p&gt;Replace these example values. Keep an explicit timezone: &lt;a href="https://docs.docker.com/reference/cli/docker/system/events/" rel="noopener noreferrer"&gt;Docker events&lt;/a&gt; otherwise interprets timestamps in the client's local timezone.&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="nv"&gt;CONTAINER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replace-with-name-or-id'&lt;/span&gt;
&lt;span class="nv"&gt;SINCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-10-03T18:20:00-05:00'&lt;/span&gt;
&lt;span class="nv"&gt;UNTIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-10-03T18:30:00-05:00'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, read the stopped container's state and configured limits with &lt;a href="https://docs.docker.com/reference/cli/docker/inspect/" rel="noopener noreferrer"&gt;Docker inspect&lt;/a&gt;. Save the full container ID with the output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'id={{.Id}} name={{.Name}} image={{.Config.Image}} exit={{.State.ExitCode}} oom_killed={{.State.OOMKilled}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} error={{json .State.Error}} memory={{.HostConfig.Memory}} reservation={{.HostConfig.MemoryReservation}} memory_swap={{.HostConfig.MemorySwap}} oom_kill_disable={{json .HostConfig.OomKillDisable}} restart_count={{.RestartCount}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, collect the event sequence. Docker exposes separate &lt;code&gt;oom&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;, &lt;code&gt;die&lt;/code&gt;, &lt;code&gt;stop&lt;/code&gt; and &lt;code&gt;restart&lt;/code&gt; events. Its retained history covers only the last 256 events. Collect it promptly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SINCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UNTIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;container&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .}}'&lt;/span&gt;

docker logs &lt;span class="nt"&gt;--timestamps&lt;/span&gt; &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SINCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UNTIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application logs can preserve the last completed operation or a shutdown request. A missing final message does not establish a kernel kill.&lt;/p&gt;

&lt;p&gt;On a systemd host, collect kernel and daemon logs too. The UTC timestamps below cover the same example interval. These queries select the current boot; &lt;a href="https://www.freedesktop.org/software/systemd/man/255/journalctl.html" rel="noopener noreferrer"&gt;journalctl documents time and boot filtering&lt;/a&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="nv"&gt;JOURNAL_SINCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-10-03 23:20:00 UTC'&lt;/span&gt;
&lt;span class="nv"&gt;JOURNAL_UNTIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2026-10-03 23:30:00 UTC'&lt;/span&gt;

journalctl &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="nt"&gt;--boot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOURNAL_SINCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOURNAL_UNTIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'out of memory|oom-kill|killed process|memory cgroup'&lt;/span&gt;

journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; docker.service &lt;span class="nt"&gt;--boot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOURNAL_SINCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOURNAL_UNTIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the incident preceded this boot, run &lt;code&gt;journalctl --list-boots&lt;/code&gt;. Choose the boot containing the incident and replace &lt;code&gt;--boot=0&lt;/code&gt; in both commands with &lt;code&gt;--boot=THE_32_CHARACTER_BOOT_ID&lt;/code&gt;. On other hosts, use their actual kernel and daemon log mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the memory boundary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;State.OOMKilled=true&lt;/code&gt; means Docker recorded an OOM-killed state. It does not establish whether the container's own limit, a parent cgroup, or host pressure was responsible. Correlate it with the event timeline and kernel record.&lt;/p&gt;

&lt;p&gt;A false flag is not an all-clear. Without matching evidence, keep the cause unresolved.&lt;/p&gt;

&lt;p&gt;The inspect field &lt;code&gt;memory&lt;/code&gt; is the container's hard limit in bytes. Zero means Docker configured no per-container hard limit; it says nothing about a parent boundary or the host's capacity. &lt;a href="https://docs.docker.com/engine/containers/resource_constraints/" rel="noopener noreferrer"&gt;Docker distinguishes hard limits from soft reservations&lt;/a&gt; and warns about host OOM when memory is exhausted.&lt;/p&gt;

&lt;p&gt;Where the host retains the container's cgroup, inspect that too. Locate the path through the host's runtime configuration. Do not borrow another machine's path.&lt;/p&gt;

&lt;p&gt;For cgroup v2:&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="nv"&gt;CGROUP_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/replace/with/verified/container-cgroup-path'&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CGROUP_PATH&lt;/span&gt;&lt;span class="s2"&gt;/memory.max"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CGROUP_PATH&lt;/span&gt;&lt;span class="s2"&gt;/memory.high"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CGROUP_PATH&lt;/span&gt;&lt;span class="s2"&gt;/memory.events"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CGROUP_PATH&lt;/span&gt;&lt;span class="s2"&gt;/memory.events.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html#memory-interface-files" rel="noopener noreferrer"&gt;kernel's memory-controller documentation&lt;/a&gt; gives these files distinct meanings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory.max&lt;/code&gt; is the hard limit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.high&lt;/code&gt; applies reclaim pressure without itself invoking the OOM killer.&lt;/li&gt;
&lt;li&gt;An increase in the exact cgroup's local &lt;code&gt;oom&lt;/code&gt; counter indicates allocation was about to fail at that cgroup's limit.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;oom_kill&lt;/code&gt; increase counts processes in the cgroup killed by an OOM killer. It does not identify the limiting boundary or prove the victim was the container's PID 1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory.events&lt;/code&gt; includes descendant events; &lt;code&gt;memory.events.local&lt;/code&gt; stays local to that cgroup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record unavailable counters as unavailable. A replacement container's counters cannot fill the gap. Hosts using cgroup v1 need its &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v1/memory.html" rel="noopener noreferrer"&gt;different memory interfaces&lt;/a&gt;, covered in the original guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a change the evidence supports
&lt;/h2&gt;

&lt;p&gt;A matching Docker &lt;code&gt;oom&lt;/code&gt; event and a nonzero limit still need corroboration. A kernel record naming the limiting cgroup, or a same-window increase in that exact cgroup's local &lt;code&gt;oom&lt;/code&gt; counter, helps establish the boundary. Then compare workload peaks with the limit and investigate the application's memory configuration or a leak.&lt;/p&gt;

&lt;p&gt;For host pressure, inventory the other workloads and host reserve. Raising one container's limit can move pressure onto the host.&lt;/p&gt;

&lt;p&gt;For a stop-timeout escalation, investigate shutdown handling. Docker notes that shell-form &lt;code&gt;ENTRYPOINT&lt;/code&gt; and &lt;code&gt;CMD&lt;/code&gt; run below &lt;code&gt;/bin/sh -c&lt;/code&gt;, which does not pass signals to the executable. Investigate signal handling or the grace period before changing memory.&lt;/p&gt;

&lt;p&gt;For an external kill, trace the actor through daemon, service or scheduler records. If no branch is proven, retain the evidence and improve capture for the next occurrence.&lt;/p&gt;

&lt;p&gt;Keep a short incident record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container ID and image:
Incident timestamp and timezone:
ExitCode / OOMKilled / RestartCount:
Configured container and proven parent limits:
Same-window Docker events:
Kernel victim and limiting cgroup, if identified:
cgroup version, exact path and counter changes:
Unavailable evidence:
Current classification, including unknown:
One change to test / result / timestamp:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands above collect evidence. They do not restart the container or change its limits. A restart may restore service while the cause remains unknown.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: AI agents adapted this article from TechFuelHQ's sourced guide. The original documents a Docker and Linux documentation review on October 3, 2026; no Docker-host OOM experiment was performed for it, and this adaptation claims no new testing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Self-hosting break-even: $23/month of SaaS, a $170 mini PC, about 9 months</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Thu, 01 Oct 2026 01:16:42 +0000</pubDate>
      <link>https://dev.to/techfuelhq/self-hosting-break-even-23month-of-saas-a-170-mini-pc-about-9-months-353o</link>
      <guid>https://dev.to/techfuelhq/self-hosting-break-even-23month-of-saas-a-170-mini-pc-about-9-months-353o</guid>
      <description>&lt;p&gt;Replacing $23/month in SaaS subscriptions (Google One 2TB + 1Password + Plex Pass) with a $170 mini PC drawing 15W idle breaks even in about 9 months. Replacing $10/month in Google storage with an $800 server never pays back.&lt;/p&gt;

&lt;p&gt;Both answers come out of the self-hosting break-even calculator on my site. The gap between them is the argument. What you're replacing is the most important variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ongoing side still has a bill
&lt;/h2&gt;

&lt;p&gt;Self-hosting doesn't cost zero after the hardware. For a minimal stack it looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost item&lt;/th&gt;
&lt;th&gt;Typical amount&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Electricity&lt;/td&gt;
&lt;td&gt;$1–4/mo&lt;/td&gt;
&lt;td&gt;10–30W idle × 24/7 at $0.184/kWh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2 backup&lt;/td&gt;
&lt;td&gt;$0.60–2/mo&lt;/td&gt;
&lt;td&gt;~100–300GB after deduplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain name&lt;/td&gt;
&lt;td&gt;$1–1.25/mo&lt;/td&gt;
&lt;td&gt;$12–15/yr amortized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total ongoing&lt;/td&gt;
&lt;td&gt;$3–7/mo&lt;/td&gt;
&lt;td&gt;Before hardware amortization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Electricity is the biggest line. Its rate matters more than most guides acknowledge. The default is $0.184/kWh, the EIA May 2026 US residential average, where a 15W mini PC running 24/7 costs about $24/year, while at $0.13/kWh (roughly the cheapest US states, Idaho and Utah, in EIA's May 2026 residential table) the same box costs about $17/year and at California's $0.333/kWh it costs about $44/year. Use the rate on your own bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions, kept apart on purpose
&lt;/h2&gt;

&lt;p&gt;The calculator answers two different questions from the same inputs. The monthly self-host figure and the year-by-year table amortize the hardware over the lifetime you pick, so a $300 mini PC used for 5 years costs $5/month in hardware amortization and the same box used for 7 years costs $3.57/month. Break-even treats the hardware as upfront instead. It's the number of months of operating savings (SaaS cost minus electricity, maintenance and backup, with hardware amortization excluded) that it takes to recoup the purchase price.&lt;/p&gt;

&lt;p&gt;Here are the calculator's own outputs for its defaults: Google One 2TB at $9.99/mo, Plex Pass at $6.99/mo and 1Password Families at $5.99/mo, a 5-year lifetime, 15 W idle, $0.184/kWh, $15/yr maintenance and Backblaze B2 at $1.20/mo. Only the hardware changes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hardware&lt;/th&gt;
&lt;th&gt;SaaS / month&lt;/th&gt;
&lt;th&gt;Self-host / month&lt;/th&gt;
&lt;th&gt;Monthly savings&lt;/th&gt;
&lt;th&gt;Break-even&lt;/th&gt;
&lt;th&gt;5 years&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$300 mini PC (default)&lt;/td&gt;
&lt;td&gt;$22.97&lt;/td&gt;
&lt;td&gt;$9.46&lt;/td&gt;
&lt;td&gt;+$13.51&lt;/td&gt;
&lt;td&gt;1.4 yr&lt;/td&gt;
&lt;td&gt;+$810&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$170 N150 mini PC&lt;/td&gt;
&lt;td&gt;$22.97&lt;/td&gt;
&lt;td&gt;$7.30&lt;/td&gt;
&lt;td&gt;+$15.67&lt;/td&gt;
&lt;td&gt;9 mo&lt;/td&gt;
&lt;td&gt;+$940&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the $170 box the ongoing costs come to about $4.50/month (electricity $2.01, maintenance $1.25, off-site backup $1.20), or about $7.30/month once the $170 is spread over five years.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the answer is "Never"
&lt;/h2&gt;

&lt;p&gt;Now the $800 server against $10/month of Google storage. It never pays back. The operating savings alone would need about 12 years, more than two 5-year hardware lifetimes, so the calculator returns "Never" rather than a payback date.&lt;/p&gt;

&lt;p&gt;That's deliberate. When break-even lands past the hardware lifetime you selected, the tool reports "Never", because a payback you can only reach after buying a replacement machine is not a payback. It's the same condition as the monthly-savings figure going negative.&lt;/p&gt;

&lt;p&gt;Flip the inputs and it gets quick. Replacing $50/month in cloud subscriptions with a $200 mini PC breaks even in under 5 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's worth replacing
&lt;/h2&gt;

&lt;p&gt;The clear wins, where self-hosting is dramatically cheaper and reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vaultwarden for 1Password Families or Bitwarden Premium&lt;/li&gt;
&lt;li&gt;Jellyfin for Plex Pass, with no transcoding session limits&lt;/li&gt;
&lt;li&gt;Uptime Kuma for UptimeRobot Solo&lt;/li&gt;
&lt;li&gt;Immich for Google One 2TB storage, if you have local storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nextcloud (for Dropbox or Google Drive) and n8n (for Zapier) are viable but take more effort. Nextcloud needs maintenance, and n8n has a steeper setup curve.&lt;/p&gt;

&lt;p&gt;I'd keep paying for three things. Email, because of deliverability problems from residential IPs and high maintenance. CDN and DDoS protection, which aren't worth running yourself. Calendar sync, which is too fragile for too little savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the defaults are assumptions
&lt;/h2&gt;

&lt;p&gt;The subscription presets use the vendors' published list prices at capture time. Plans change.&lt;/p&gt;

&lt;p&gt;So confirm the current price before deciding, and treat the rest of the defaults the same way. The $0.184/kWh default is an average; your bill's effective rate is the number to use. The 15 W idle draw, $15/year maintenance and $1.20/month backup are my working assumptions for a small mini-PC stack, stated so you can replace them with your own measured numbers. The six hardware presets are street-price estimates for a class of machine. They are not quotes for a named SKU, and they aren't sourced the way the subscription prices are. Type in what you actually paid.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First published on my site, &lt;a href="https://techfuelhq.com/tools/selfhost-breakeven/?utm_source=dev-to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=social-desk-2026-09&amp;amp;utm_content=selfhost-breakeven" rel="noopener noreferrer"&gt;techfuelhq.com&lt;/a&gt;. AI agents adapted this post from that page, and every number here is copied from it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>homelab</category>
      <category>saas</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>Raw data is useful only when the caveats travel with it</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:03:54 +0000</pubDate>
      <link>https://dev.to/techfuelhq/raw-data-is-useful-only-when-the-caveats-travel-with-it-6cj</link>
      <guid>https://dev.to/techfuelhq/raw-data-is-useful-only-when-the-caveats-travel-with-it-6cj</guid>
      <description>&lt;p&gt;When comparing public benchmark datasets, the method and limitations should travel with the download. That is the point of the TechFuelHQ dataset hub: the evidence trail is part of the data.&lt;/p&gt;

&lt;p&gt;Use it this way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the dataset page before the file. Check the collection date, test setup, attribution, correction history, and dataset-specific license.&lt;/li&gt;
&lt;li&gt;Download the CSV or JSON. CSVs work in a spreadsheet or with pandas; JSON works well in scripts and small tools. Keep blanks as blanks instead of silently converting “not measured” into zero.&lt;/li&gt;
&lt;li&gt;Follow the reproduction material. Depending on the release, that may be an evidence manifest, a corpus/scorer README, or a Python runner with per-repetition output. Read code before running it and preserve the documented hardware, model, and settings when comparing results.&lt;/li&gt;
&lt;li&gt;Cite the dataset version and capture date in anything you publish.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hub currently includes first-party GPU and local-LLM measurements, Home Assistant voice results, component-reference JSON, and price-observation CSVs. A price row describes its capture date; it is not a live-price claim. The PC-builder reference data stores compatibility attributes and source pages without storing prices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://techfuelhq.com/data/?utm_source=dev-to&amp;amp;utm_medium=social&amp;amp;utm_campaign=distribution-20260920&amp;amp;utm_content=data-downloads" rel="noopener noreferrer"&gt;Browse or download the files&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: This is my site. This post was prepared and checked by AI agents; the file types and resources described here were checked against the published hub.&lt;/p&gt;

</description>
      <category>data</category>
    </item>
    <item>
      <title>How to Test a UPS: Charge Is Not Health</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Wed, 16 Sep 2026 19:31:55 +0000</pubDate>
      <link>https://dev.to/techfuelhq/how-to-test-a-ups-charge-is-not-health-nb4</link>
      <guid>https://dev.to/techfuelhq/how-to-test-a-ups-charge-is-not-health-nb4</guid>
      <description>&lt;p&gt;The number on the front of a UPS is a charge reading. It is not a health reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A unit showing full can drop the load the moment it transfers.&lt;/strong&gt; The display will have been telling you everything was fine right up until the instant it stopped being fine, and it was not lying — it was answering a different question than the one you thought you were asking.&lt;/p&gt;

&lt;p&gt;That gap between charged and working is the whole subject here. &lt;a href="https://techfuelhq.com/tools/ups-runtime-calculator/" rel="noopener noreferrer"&gt;Sizing the unit&lt;/a&gt; is settled territory: VA against watts, runtime against your shutdown window. This is about what comes after you have bought the right size — proving it does the job, on a schedule, before the power goes out and proves it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in self-test is not a runtime test
&lt;/h2&gt;

&lt;p&gt;Most units test themselves. The test covers less than the name suggests.&lt;/p&gt;

&lt;p&gt;Schneider's documentation for the Smart-UPS line says the unit self-tests when you switch it on and every two weeks after that, and that during the test it runs on battery for approximately ten seconds (&lt;a href="https://www.se.com/us/en/faqs/FA405317/" rel="noopener noreferrer"&gt;Schneider Electric FA405317&lt;/a&gt;). The Back-UPS line runs the same ten seconds on a different clock — Schneider describes the automatic test as happening every seven days after the unit is powered on — and its replace-battery alarm is framed against a standard two-minute runtime limit (&lt;a href="https://www.se.com/us/en/faqs/FAQ000267818/" rel="noopener noreferrer"&gt;Schneider Electric FAQ000267818&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Ten seconds is a genuinely useful test of one thing: the transfer relay closes and the inverter comes up. Keep it enabled. But read what it is measuring. A battery that has lost most of its useful capacity will hold ten seconds without complaint, and it will hold two minutes for a while after that. By the time the automatic test starts failing, you have already been running on a battery that would not have finished a shutdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Charge is a percentage. Capacity is minutes. The panel shows you the first one and you need the second one.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  An untested UPS is a surge protector with a battery in it
&lt;/h2&gt;

&lt;p&gt;So test on a schedule, and make it a real test.&lt;/p&gt;

&lt;p&gt;Connect the load the unit actually protects. Pull the input cord. Watch what the load does at the moment of transfer, and time how long the battery holds before the shutdown you configured actually fires. That one pull exercises the transfer, the inverter, the battery under its real load, and your shutdown chain, in the order they will happen for real.&lt;/p&gt;

&lt;p&gt;Nothing else does. A ten-second self-test skips the load. A runtime figure from a spec sheet skips your gear. The &lt;a href="https://techfuelhq.com/tools/ups-runtime-calculator/" rel="noopener noreferrer"&gt;UPS runtime calculator&lt;/a&gt; will model the curve for you, and the model is worth having as a starting expectation — but a modelled forty minutes and a measured forty minutes are not the same claim, and only one of them is about the battery currently sitting in your rack.&lt;/p&gt;

&lt;p&gt;If you would rather not pull a cord on live equipment, NUT will start a deep battery test on hardware that exposes one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;upscmd &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;user&amp;gt; &amp;lt;upsname&amp;gt; test.battery.start.deep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;test.battery.start.quick&lt;/code&gt;, &lt;code&gt;test.battery.start.deep&lt;/code&gt; and &lt;code&gt;test.battery.stop&lt;/code&gt; are all standard NUT instant commands (&lt;a href="https://networkupstools.org/docs/developer-guide.chunked/_instant_commands_4.html" rel="noopener noreferrer"&gt;NUT instant command reference&lt;/a&gt;), and &lt;code&gt;upscmd -l &amp;lt;upsname&amp;gt;&lt;/code&gt; lists what your specific unit supports (&lt;a href="https://networkupstools.org/docs/man/upscmd.html" rel="noopener noreferrer"&gt;upscmd man page&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There is a second reason to test that has nothing to do with the battery. &lt;strong&gt;A UPS can fail in a way that takes the load down with it.&lt;/strong&gt; That is a real failure mode, not a freak event. The box you bought to prevent an unclean shutdown sits in series with everything it protects, and the only way to find out how yours behaves when it goes wrong is to make it transfer while you are standing there rather than at 3am while you are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pure sine, or don't put a server PSU behind it
&lt;/h2&gt;

&lt;p&gt;This one has a mechanism, and the mechanism is published by the people selling the cheaper option.&lt;/p&gt;

&lt;p&gt;CyberPower's explanation of its own PFC Sinewave line is that a simulated sine wave produces a zero-output state during the phase change — a gap in the waveform — and that this gap can interrupt equipment running an active-PFC power supply when the unit switches from mains to battery (&lt;a href="https://www.cyberpowersystems.com/news/pfc-sinewave-series-ups-systems-the-most-affordable-feature-rich-pure-sine-wave-ups/" rel="noopener noreferrer"&gt;CyberPower, PFC Sinewave Series&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Read where that failure lands. Not during a brownout, not on a slow degrade — at transfer. The single event the UPS exists to handle is the event a simulated-sine unit is worst at, and active PFC is standard in modern PC and server supplies; CyberPower's own page dates that transition to the last fifteen years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulated sine into an active-PFC supply is asking for trouble on transfer. Pure sine for anything with a server PSU behind it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  USB and NUT, not a network management card
&lt;/h2&gt;

&lt;p&gt;Network management cards cost absurd money next to the UPS they plug into. A &lt;a href="https://www.se.com/us/en/product/AP9641/ups-network-management-card-3-with-environmental-monitoring/" rel="noopener noreferrer"&gt;UPS network management card&lt;/a&gt; is a separate accessory bought on top of the unit, and for a single-site homelab it buys network-side monitoring you can get from a cable.&lt;/p&gt;

&lt;p&gt;USB plus &lt;a href="https://networkupstools.org/" rel="noopener noreferrer"&gt;NUT&lt;/a&gt; does the job and is what I'd run. Three pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;usbhid-ups&lt;/code&gt;&lt;/strong&gt; should detect anything implementing the HID Power Device Class, with the amount of data varying by manufacturer and model. NUT lists all MGE, Dell and AMETEK Powervar USB models, the newer Eaton USB models, and specific models from APC, CyberPower, TrippLite, Belkin, Powercom and PowerWalker (&lt;a href="https://networkupstools.org/docs/man/usbhid-ups.html" rel="noopener noreferrer"&gt;usbhid-ups man page&lt;/a&gt;). Check your exact model against the compatibility list before you count on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;upsmon&lt;/code&gt;&lt;/strong&gt; watches for the critical condition — on battery, low charge — sets the forced-shutdown flag, and lets every machine it is monitoring run its own shutdown command with a delay you set (&lt;a href="https://networkupstools.org/docs/user-manual.chunked/Configuration_notes.html" rel="noopener noreferrer"&gt;NUT configuration notes&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;upscmd&lt;/code&gt;&lt;/strong&gt; gives you the battery tests above and the alarm control below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the entire feature set most homelabs wanted from the card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If it can't trigger a clean shutdown of what's plugged into it, most of the value is gone.&lt;/strong&gt; A UPS that only buys you a few minutes of uptime during which nobody is home to act is a very expensive way to delay the same unclean power-off. The shutdown chain is the product. Configure it the day the unit arrives, then test that it fires — which is the same plug-pull from two sections up, and the reason that test is worth doing properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alarm will go off at 3am
&lt;/h2&gt;

&lt;p&gt;It will. The alarm is standard equipment, and the first time it fires is the worst possible moment to be learning where the mute button is.&lt;/p&gt;

&lt;p&gt;Decide now what you want it to do, and set it now. NUT exposes &lt;code&gt;beeper.disable&lt;/code&gt;, &lt;code&gt;beeper.mute&lt;/code&gt;, &lt;code&gt;beeper.enable&lt;/code&gt; and &lt;code&gt;beeper.toggle&lt;/code&gt; as instant commands (&lt;a href="https://networkupstools.org/docs/developer-guide.chunked/_instant_commands_4.html" rel="noopener noreferrer"&gt;same reference&lt;/a&gt;), so this is one &lt;code&gt;upscmd&lt;/code&gt; call, done while you are awake and thinking clearly about whether you actually want an audible alarm in that room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batteries are a consumable
&lt;/h2&gt;

&lt;p&gt;Plan for the replacement rather than being surprised by it. Sealed lead-acid cells are on a several-year cycle, and third-party replacements are standard practice — not a corner cut.&lt;/p&gt;

&lt;p&gt;Schneider puts most APC batteries at three to five years and notes that every 8°C (15°F) of extra operating temperature halves that life (&lt;a href="https://www.se.com/us/en/faqs/FA158934/" rel="noopener noreferrer"&gt;Schneider Electric FA158934&lt;/a&gt;). That second figure is the one people miss. A UPS in a warm closet is not on the same schedule as the same UPS in a basement, and if you have put yours in the warm closet, the replacement year on your calendar is optimistic.&lt;/p&gt;

&lt;p&gt;Which puts you back at the test. The only way to know where your battery is on that curve is to have measured it recently under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do: pure sine, USB, NUT, and a plug-pull on a schedule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Buy a pure sine wave unit sized to your load, connect it over USB, run NUT, and pull the plug on a schedule.&lt;/strong&gt; No management card. That configuration covers the transfer, the shutdown, the alarm and the tests, and there is nothing left in the card that a homelab with one rack needs.&lt;/p&gt;

&lt;p&gt;The plug-pull is the part people skip and the only part that produces information. Put it on a recurring reminder alongside whatever else you check, keep the number you measured, and compare it to the last one. When the measured runtime has fallen far enough that your shutdown no longer finishes inside it, buy cells — before the automatic self-test gets around to agreeing with you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is wrong for a few situations.&lt;/strong&gt; Simulated sine into active PFC — do not, at any price. A consumer unit under a full rack, where the load is past what the unit is rated to hold and the runtime is theatre. And anyone who will not set up automatic shutdown, because at that point the money bought a surge protector with a battery in it, which is where this started.&lt;/p&gt;

&lt;p&gt;Testing does not make a UPS more reliable. It tells you whether the one you own is still doing the job, which is a different and more useful thing than a green light. And none of it replaces keeping backups somewhere else — a clean shutdown protects the pool, not the data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://techfuelhq.com/homelab/ups-testing-reality-2026/" rel="noopener noreferrer"&gt;techfuelhq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>hardware</category>
      <category>sysadmin</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How to Install Proxmox VE (2026): Install + Update Reference</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Sat, 04 Jul 2026 16:35:39 +0000</pubDate>
      <link>https://dev.to/techfuelhq/how-to-install-proxmox-ve-2026-install-update-reference-2ak8</link>
      <guid>https://dev.to/techfuelhq/how-to-install-proxmox-ve-2026-install-update-reference-2ak8</guid>
      <description>&lt;p&gt;Installing Proxmox VE is genuinely easy. The graphical installer is a few screens, and it writes itself to disk in a couple of minutes. What trips people up is not the install itself: it is the first fifteen minutes afterward, where a default install points at a repository you cannot use without paying, and the longer-term question of how you keep the thing updated once it is running.&lt;/p&gt;

&lt;p&gt;This is the generic reference for both. It is hardware-agnostic on purpose: the steps below apply to a mini PC, a used office desktop, an old gaming rig, or a rack server. If you want a hands-on, click-by-click walkthrough on a specific small box, the &lt;a href="https://techfuelhq.com/articles/proxmox-mini-pc-setup/" rel="noopener noreferrer"&gt;Proxmox mini PC setup guide&lt;/a&gt; is the hardware-specific companion to this one. If you are still choosing hardware, start with the &lt;a href="https://techfuelhq.com/homelab/proxmox-hardware-requirements-2026/" rel="noopener noreferrer"&gt;Proxmox hardware requirements&lt;/a&gt;. And if you already run VE 8 and want to jump to VE 9, that is a different, planned operation covered in the &lt;a href="https://techfuelhq.com/tutorials/proxmox-ve-8-to-9-upgrade-2026/" rel="noopener noreferrer"&gt;8-to-9 upgrade gotchas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything below is cited to the official Proxmox documentation. The current release as of this writing is Proxmox VE 9.2-1 (released May 21, 2026), built on Debian 13 "Trixie." If a command or file path is here, it is real and current for VE 9.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The clean install (any hardware)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Download and verify the ISO
&lt;/h3&gt;

&lt;p&gt;Grab the latest Proxmox VE ISO from the &lt;a href="https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso" rel="noopener noreferrer"&gt;official downloads page&lt;/a&gt;. As of June 2026 that is the VE 9.2-1 installer, about 1.7 GB.&lt;/p&gt;

&lt;p&gt;Do not skip the checksum. The download page publishes a SHA256 value next to the ISO. After downloading, compute the hash and compare it to that published value:&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;# Linux / macOS&lt;/span&gt;
&lt;span class="nb"&gt;sha256sum &lt;/span&gt;proxmox-ve_9.2-1.iso

&lt;span class="c"&gt;# Windows (PowerShell)&lt;/span&gt;
Get-FileHash proxmox-ve_9.2-1.iso &lt;span class="nt"&gt;-Algorithm&lt;/span&gt; SHA256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the two strings do not match, the download is corrupt or tampered with. Delete it and re-download. This takes ten seconds and it is the one verification step that matters before you write installation media.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Write the USB installer
&lt;/h3&gt;

&lt;p&gt;A USB stick is the standard install medium. The tool depends on your OS, and the official guidance is specific about which ones work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux:&lt;/strong&gt; use &lt;code&gt;dd&lt;/code&gt;. Identify the USB device first with &lt;code&gt;lsblk&lt;/code&gt;, then write with &lt;code&gt;dd bs=1M conv=fdatasync if=./proxmox-ve_9.2-1.iso of=/dev/XYZ&lt;/code&gt; (replace &lt;code&gt;/dev/XYZ&lt;/code&gt; with your actual USB device; getting this wrong overwrites the wrong disk).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; Etcher works with no configuration. Rufus also works, but you must select &lt;strong&gt;DD mode&lt;/strong&gt; when it prompts, not ISO mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS:&lt;/strong&gt; convert the ISO to a raw image, then write it (see the official media-prep page for the exact &lt;code&gt;hdiutil&lt;/code&gt; and &lt;code&gt;dd&lt;/code&gt; sequence).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One specific warning from the docs: do not use UNetbootin. It is incompatible with the Proxmox installer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Boot the installer and pick your filesystem
&lt;/h3&gt;

&lt;p&gt;Plug the USB into the target machine, power on, and bring up the boot menu (commonly F11, F12, or Del). Select the USB device, then choose &lt;strong&gt;Install Proxmox VE&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two decisions matter. &lt;strong&gt;Target disk and filesystem:&lt;/strong&gt; select the SSD or NVMe you want Proxmox on, and be careful not to pick the USB installer drive by accident.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filesystem&lt;/th&gt;
&lt;th&gt;When to choose it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ext4 (default)&lt;/td&gt;
&lt;td&gt;Single-disk machine. Simple, fast, reliable, uses an LVM-thin pool Proxmox manages natively. The right first-install answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZFS&lt;/td&gt;
&lt;td&gt;Two or more drives and you want a mirror or RAID-Z, plus snapshots, checksums, and self-healing. Wants meaningful RAM for its cache.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XFS&lt;/td&gt;
&lt;td&gt;Offered with LVM, but far less common in Proxmox setups than ext4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Btrfs&lt;/td&gt;
&lt;td&gt;A technology preview. Not recommended for a production-style install yet.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official installer note is blunt that ZFS on top of any hardware RAID is unsupported and can cause data loss, so if you run ZFS, give it the raw disks. For a first build on one drive, choose ext4 and move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network and password:&lt;/strong&gt; set a hostname, then set a static IP outside your DHCP range so the web UI always lives at the same address. Set a strong root password. The installer builds a Linux bridge called &lt;code&gt;vmbr0&lt;/code&gt; on your first NIC so VMs and containers can reach the network. Click install; it writes to disk in a few minutes and reboots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Reach the web interface
&lt;/h3&gt;

&lt;p&gt;Point a browser on another machine at the IP given during install, on port 8006:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-ip:8006
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;strong&gt;https&lt;/strong&gt; and the &lt;strong&gt;:8006&lt;/strong&gt;. Plain http or a missing port is the single most common "it didn't work" mistake, and the install is almost certainly fine. You will get a certificate warning because Proxmox uses a self-signed cert; accept it and log in as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: The first fifteen minutes (the repository fix)
&lt;/h2&gt;

&lt;p&gt;This is the part generic install guides skip, and it is the one step every Proxmox install needs. A fresh install points apt at the &lt;strong&gt;enterprise repository&lt;/strong&gt;, which requires a paid subscription. Without a key, every &lt;code&gt;apt update&lt;/code&gt; ends in &lt;code&gt;401 Unauthorized&lt;/code&gt; and your package lists never refresh.&lt;/p&gt;

&lt;p&gt;Proxmox VE 9 also changed the repository file format to the deb822 &lt;code&gt;.sources&lt;/code&gt; format, so the files look nothing like the one-line &lt;code&gt;sources.list&lt;/code&gt; entries from VE 8.&lt;/p&gt;

&lt;h3&gt;
  
  
  Disable the enterprise repository
&lt;/h3&gt;

&lt;p&gt;Open the node shell (web UI, your node, Shell) and edit &lt;code&gt;/etc/apt/sources.list.d/pve-enterprise.sources&lt;/code&gt;, adding a single line so it reads &lt;code&gt;Enabled: no&lt;/code&gt;. That suppresses the enterprise repo without deleting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the no-subscription repository
&lt;/h3&gt;

&lt;p&gt;Create the no-subscription sources file with the exact deb822 contents the docs publish for VE 9:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/apt/sources.list.d/proxmox.sources
&lt;/span&gt;&lt;span class="py"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deb&lt;/span&gt;
&lt;span class="py"&gt;URIs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://download.proxmox.com/debian/pve&lt;/span&gt;
&lt;span class="py"&gt;Suites&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trixie&lt;/span&gt;
&lt;span class="py"&gt;Components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pve-no-subscription&lt;/span&gt;
&lt;span class="py"&gt;Signed-By&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/share/keyrings/proxmox-archive-keyring.gpg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can do all of this from the web UI instead: node, &lt;strong&gt;Updates&lt;/strong&gt;, &lt;strong&gt;Repositories&lt;/strong&gt;, &lt;strong&gt;Add&lt;/strong&gt;, pick &lt;code&gt;No-Subscription&lt;/code&gt;, then highlight the enterprise row and click &lt;strong&gt;Disable&lt;/strong&gt;. The GUI writes the same files for you.&lt;/p&gt;

&lt;p&gt;The no-subscription repo ships the same packages as enterprise without a key. Proxmox is explicit that these are "not always as heavily tested and validated," so it is fine for a home lab but is not their recommendation for production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the first update
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update
apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;full-upgrade&lt;/code&gt;, not plain &lt;code&gt;upgrade&lt;/code&gt;, more on why in Part 3. Reboot if a new kernel was installed. The subscription-nag popup at login is cosmetic; click OK and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: How to update Proxmox over time
&lt;/h2&gt;

&lt;p&gt;Once Proxmox is running, "update" means two very different things, and conflating them is where homelabs get into trouble.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routine updates (within a major version)
&lt;/h3&gt;

&lt;p&gt;Normal maintenance, keeping VE 9 current as point releases ship (9.1 to 9.2). From the shell, &lt;code&gt;apt update&lt;/code&gt; then &lt;code&gt;apt full-upgrade&lt;/code&gt;; or in the web UI, select the node, open &lt;strong&gt;Updates&lt;/strong&gt;, &lt;strong&gt;Refresh&lt;/strong&gt;, then &lt;strong&gt;Upgrade&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;full-upgrade&lt;/code&gt; and not &lt;code&gt;apt upgrade&lt;/code&gt;? Plain &lt;code&gt;upgrade&lt;/code&gt; refuses to install anything that requires a new dependency, and on Proxmox that means the kernel and the &lt;code&gt;pve-manager&lt;/code&gt; stack get held back, exactly the packages you want updated. &lt;code&gt;full-upgrade&lt;/code&gt; installs them. A reboot is only needed when a new kernel arrives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Major upgrades (across major versions)
&lt;/h3&gt;

&lt;p&gt;Moving from VE 8 to VE 9 is a different animal. It crosses to a new major version, which moves the underlying Debian base and kernel, and it carries real breaking changes: LVM autoactivation defaults change, network interfaces can be renamed, and PCI passthrough can need attention. That is a planned event with its own pre-flight checklist: back up first, run the &lt;code&gt;pve8to9 --full&lt;/code&gt; checker, pin your NIC names, and have console access that is not the web GUI. The full procedure is in the &lt;a href="https://techfuelhq.com/tutorials/proxmox-ve-8-to-9-upgrade-2026/" rel="noopener noreferrer"&gt;Proxmox VE 8 to 9 upgrade guide&lt;/a&gt;. VE 8.4 gets security and bug-fix updates until August 2026, so there is no pressure to rush.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common install gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apt update&lt;/code&gt; returns 401 Unauthorized.&lt;/strong&gt; You are still on the enterprise repository. Disable it and enable &lt;code&gt;pve-no-subscription&lt;/code&gt; (Part 2). This is the number-one new-user issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The web UI will not load.&lt;/strong&gt; You used &lt;code&gt;http&lt;/code&gt; instead of &lt;code&gt;https&lt;/code&gt;, or forgot &lt;code&gt;:8006&lt;/code&gt;. The correct URL is &lt;code&gt;https://your-ip:8006&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VMs will not start ("KVM not available").&lt;/strong&gt; Hardware virtualization (Intel VT-x or AMD-V) is disabled in the BIOS/UEFI. Reboot into firmware and enable it, plus IOMMU (VT-d / AMD-Vi) if you plan passthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You picked ZFS and the host feels short on RAM.&lt;/strong&gt; ZFS reserves RAM for its cache. Size it deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You accidentally installed onto the USB stick.&lt;/strong&gt; Always confirm the target disk in the installer is your internal SSD/NVMe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  After the install: what to build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a screen-by-screen walkthrough on a specific small machine, follow the &lt;a href="https://techfuelhq.com/articles/proxmox-mini-pc-setup/" rel="noopener noreferrer"&gt;Proxmox mini PC setup guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For each service, decide between a container and a full VM. The &lt;a href="https://techfuelhq.com/homelab/lxc-vs-vm-proxmox-2026/" rel="noopener noreferrer"&gt;LXC vs VM in Proxmox guide&lt;/a&gt; has the decision tree.&lt;/li&gt;
&lt;li&gt;To size your box, how many VMs and containers your RAM and cores can support with ZFS overhead included, the &lt;a href="https://techfuelhq.com/tools/proxmox-capacity-planner/" rel="noopener noreferrer"&gt;Proxmox capacity planner&lt;/a&gt; does the math.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The install is the easy part. The repository fix is what makes updates work, and knowing the difference between a routine update and a major upgrade is what keeps the box healthy for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pve.proxmox.com/wiki/Package_Repositories" rel="noopener noreferrer"&gt;Proxmox VE Package Repositories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pve.proxmox.com/wiki/Installation" rel="noopener noreferrer"&gt;Proxmox VE Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pve.proxmox.com/wiki/Prepare_Installation_Media" rel="noopener noreferrer"&gt;Proxmox VE: Prepare Installation Media&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso" rel="noopener noreferrer"&gt;Proxmox VE downloads (ISO)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pve.proxmox.com/wiki/Upgrade_from_8_to_9" rel="noopener noreferrer"&gt;Proxmox VE 8 to 9 Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://techfuelhq.com/articles/how-to-install-proxmox-ve-2026/" rel="noopener noreferrer"&gt;techfuelhq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>proxmox</category>
      <category>selfhosted</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
