<?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>Proxmox LXC or VM: a per-service table, a decision tree and the gotchas</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Tue, 10 Nov 2026 03:56:23 +0000</pubDate>
      <link>https://dev.to/techfuelhq/proxmox-lxc-or-vm-a-per-service-table-a-decision-tree-and-the-gotchas-7dg</link>
      <guid>https://dev.to/techfuelhq/proxmox-lxc-or-vm-a-per-service-table-a-decision-tree-and-the-gotchas-7dg</guid>
      <description>&lt;p&gt;Adapted from &lt;a href="https://techfuelhq.com/homelab/lxc-vs-vm-proxmox-2026/?utm_source=dev-to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=distribution-2026-11&amp;amp;utm_content=lxc-vm-service-table" rel="noopener noreferrer"&gt;LXC vs VM in Proxmox: When to Use Each&lt;/a&gt;, published on TechFuelHQ.&lt;/p&gt;

&lt;p&gt;The kernel decides it. A Proxmox VM boots its own kernel on virtual hardware. An LXC container, which Proxmox calls a CT and manages with &lt;code&gt;pct&lt;/code&gt; (the Proxmox Container Toolkit), runs on the host's. So a container runs only Linux and can't live-migrate, while a VM runs Windows or FreeBSD as readily as Linux and can take a whole PCI device. The Proxmox behaviour below is from Proxmox VE 9.2 (released May 21, 2026) and its 9.2.12 manual, dated September 18, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one for each service
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Put it in&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker host (Compose stacks)&lt;/td&gt;
&lt;td&gt;VM&lt;/td&gt;
&lt;td&gt;Proxmox's container chapter keeps nesting application containers inside a QEMU VM as a recommended practice for maximum isolation and live migration. Docker in an unprivileged LXC runs with &lt;code&gt;nesting=1,keyctl=1&lt;/code&gt;. I'd still put a Compose stack in a VM.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home Assistant&lt;/td&gt;
&lt;td&gt;VM running HAOS&lt;/td&gt;
&lt;td&gt;Home Assistant calls HAOS the recommended installation type for most users. A container install has no apps, and Z-Wave is controlled by an app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plex or Jellyfin on an Intel iGPU&lt;/td&gt;
&lt;td&gt;LXC&lt;/td&gt;
&lt;td&gt;Pass &lt;code&gt;/dev/dri/renderD128&lt;/code&gt; in as a device with the render group's GID. The driver stays on the host.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plex or Jellyfin on a dedicated NVIDIA card&lt;/td&gt;
&lt;td&gt;VM, when one guest gets the card&lt;/td&gt;
&lt;td&gt;With PCI passthrough the VM owns the card, and the host and other VMs can't use it. Sharing it with an LXC instead needs the NVIDIA kernel driver on the host, since a container runs on the host's kernel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAS operating system (TrueNAS)&lt;/td&gt;
&lt;td&gt;VM with the disk controller passed through&lt;/td&gt;
&lt;td&gt;For data you care about, TrueNAS says the storage controller must be assigned to the VM via PCI passthrough. TrueNAS lists VM snapshots and live migration among what passthrough gives up; ZFS snapshots inside TrueNAS still work.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File shares from the host's own ZFS pool&lt;/td&gt;
&lt;td&gt;LXC with a bind mount&lt;/td&gt;
&lt;td&gt;The host keeps the pool and hands one directory to the container: &lt;code&gt;pct set 100 -mp0 /mnt/bindmounts/shared,mp=/shared&lt;/code&gt;. vzdump never backs up a bind mount, so the pool needs its own backup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows guest&lt;/td&gt;
&lt;td&gt;VM&lt;/td&gt;
&lt;td&gt;Proxmox's docs: other operating systems like FreeBSD or Windows can't run inside a container.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall (OPNsense, pfSense)&lt;/td&gt;
&lt;td&gt;VM&lt;/td&gt;
&lt;td&gt;Both are built on FreeBSD. Netgate says checksum offloading must be disabled on VirtIO interfaces.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS filter (Pi-hole, AdGuard Home)&lt;/td&gt;
&lt;td&gt;LXC&lt;/td&gt;
&lt;td&gt;A plain Linux service with no module, device or other OS to ask for, and a container's runtime cost is "low, usually negligible".&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything not listed goes through the tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New workload in Proxmox?
│
├─ Docker/Compose stack or Home Assistant OS? → VM
│
├─ Windows, FreeBSD or any non-Linux OS? → VM
│
├─ One guest owns a whole PCI device (GPU, HBA, NIC)? → VM
│
├─ Needs its own kernel or a module the host lacks? → VM
│
├─ Untrusted or public-facing code? → VM
│
├─ Must keep running while it moves between cluster nodes? → VM
│
└─ Everything else → LXC container
   (a Linux service, trusted code; an iGPU render node or a host directory can be passed in)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a service could go either way, I'd start it in a VM. Moving it into a container later is a planned job; finding out three months in that a container needs the whole GPU is a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the overhead looked like on my box
&lt;/h2&gt;

&lt;p&gt;Measured on a Beelink SEi12 Pro (N150, 16GB RAM, 512GB NVMe) running Proxmox VE 8.x, then the current release. Treat the absolute megabytes as 8.x-era; the container-versus-VM gap is structural, so the shape holds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;LXC (Debian 12, idle)&lt;/th&gt;
&lt;th&gt;VM (Debian 12, idle, virtio)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RAM used by guest&lt;/td&gt;
&lt;td&gt;~55 MB&lt;/td&gt;
&lt;td&gt;~280 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU overhead&lt;/td&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;td&gt;1 to 3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage IO vs bare metal&lt;/td&gt;
&lt;td&gt;~98%&lt;/td&gt;
&lt;td&gt;~94 to 97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network throughput vs bare metal&lt;/td&gt;
&lt;td&gt;~99%&lt;/td&gt;
&lt;td&gt;~97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time&lt;/td&gt;
&lt;td&gt;0.5 to 1.5 seconds&lt;/td&gt;
&lt;td&gt;8 to 20 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup size (Debian + 1 app)&lt;/td&gt;
&lt;td&gt;~400 MB&lt;/td&gt;
&lt;td&gt;~2.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The storage IO gap closes significantly when using virtio-blk with io_uring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxmox-specific gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Unprivileged is the default.&lt;/strong&gt; The container's root UID 0 is mapped to an unprivileged user on the host, by default in both the Create CT wizard and &lt;code&gt;pct create&lt;/code&gt;. The LXC project calls a privileged container unsafe and doesn't treat new escapes from one as worthy of a CVE. Keep privileged containers for trusted work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NFS or SMB inside a container.&lt;/strong&gt; The NFS and SMB/CIFS boxes in the container's Features dialog are marked "privileged only", and pct.conf(5) warns that an NFS mount can block the host's I/O completely and prevent it from rebooting. Mount the share on the Proxmox host and bind-mount it into the container. The container stays unprivileged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker inside a container.&lt;/strong&gt; An unprivileged container needs &lt;code&gt;keyctl=1&lt;/code&gt; and &lt;code&gt;nesting=1&lt;/code&gt;. The web interface has turned nesting on for new unprivileged containers since Proxmox VE 7.1. VE 9.1 can create a container straight from an OCI image, but running application containers that way is still a technology preview, and Compose support is on the roadmap, not in 9.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kernel modules.&lt;/strong&gt; A container runs on whatever modules the host kernel has. pct.conf(5) lists &lt;code&gt;force_rw_sys&lt;/code&gt;, &lt;code&gt;fuse&lt;/code&gt;, &lt;code&gt;keyctl&lt;/code&gt;, &lt;code&gt;mknod&lt;/code&gt;, &lt;code&gt;mount&lt;/code&gt; and &lt;code&gt;nesting&lt;/code&gt;, and none of them loads a module. WireGuard isn't a reason for a VM: it's been in the mainline kernel since Linux 5.6, and Proxmox VE 9.2 ships kernel 7.0. A module the Proxmox kernel doesn't carry, a different ZFS version or an out-of-tree NIC driver is a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live migration.&lt;/strong&gt; In a cluster, &lt;code&gt;qm migrate &amp;lt;vmid&amp;gt; &amp;lt;target&amp;gt; --online&lt;/code&gt; moves a running VM; passed-through PCI or USB devices block it, local disks don't. A running container can't live-migrate. &lt;code&gt;pct migrate --restart&lt;/code&gt; shuts it down (killing it after 180 seconds by default), copies it and starts it on the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshots depend on the storage.&lt;/strong&gt; Container snapshots need ZFS, LVM-thin or Ceph RBD (BTRFS is a technology preview). On directory, NFS, CIFS or plain LVM storage a container gets none. A VM also gets them on file storage when its disk is qcow2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups
&lt;/h2&gt;

&lt;p&gt;A container's snapshot-mode backup needs every backed-up volume on snapshot-capable storage; without it, suspend mode rsyncs to a temporary directory and needs space for the copy. Bind and device mounts are never backed up. A VM's snapshot mode is a live backup on any storage type, and with the guest agent running it calls &lt;code&gt;guest-fsfreeze-freeze&lt;/code&gt; and &lt;code&gt;guest-fsfreeze-thaw&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The layout I'd use: the ZFS pool stays on the Proxmox host, datasets bind-mount into unprivileged containers for native Linux services like Jellyfin or a Samba share, and Compose stacks go in a VM. The pool needs its own backup.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>proxmox</category>
      <category>homelab</category>
      <category>linux</category>
    </item>
    <item>
      <title>Jellyfin in Docker: find out which side of the container is wrong</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:39:04 +0000</pubDate>
      <link>https://dev.to/techfuelhq/jellyfin-in-docker-find-out-which-side-of-the-container-is-wrong-4d1h</link>
      <guid>https://dev.to/techfuelhq/jellyfin-in-docker-find-out-which-side-of-the-container-is-wrong-4d1h</guid>
      <description>&lt;p&gt;The setup wizard opens on port 8096, but the movie folder looks empty. Switching on hardware acceleration won't fix that. The first question is simpler: does the container see the same files you do?&lt;/p&gt;

&lt;p&gt;Suppose Compose maps &lt;code&gt;/mnt/media/movies:/data/movies&lt;/code&gt;. The left side is a directory on the host. The right side is where the Jellyfin process sees it. In the library picker, use &lt;code&gt;/data/movies&lt;/code&gt;. To compare the two views, run these from the directory containing your Compose file, assuming the service is named &lt;code&gt;jellyfin&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;ls&lt;/span&gt; /mnt/media/movies
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;jellyfin &lt;span class="nb"&gt;ls&lt;/span&gt; /data/movies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty host directory points you back to the media or NAS mount. Files on the host but no files inside? Check the bind mount in the running container. If Jellyfin still cannot read a file that this command lists, check the service user's permissions; an interactive command and the server may run as different users. There's no reason to change ownership of the whole library just to find out.&lt;/p&gt;

&lt;p&gt;If Compose never gets far enough to open the wizard, read the first network error. A sample with an external network named &lt;code&gt;proxy&lt;/code&gt; expects that network to exist already. For a LAN-only test that doesn't use a reverse proxy, remove the service's reference to &lt;code&gt;proxy&lt;/code&gt; and the top-level declaration for that network. Leave the port mapping in place.&lt;/p&gt;

&lt;p&gt;I'd leave the GPU out of this test.&lt;/p&gt;

&lt;p&gt;Once Jellyfin can find and play the file, the &lt;a href="https://techfuelhq.com/tutorials/jellyfin-setup-docker-homelab-2026/?utm_source=dev-to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=distribution-2026-11&amp;amp;utm_content=jellyfin-compose-boundaries" rel="noopener noreferrer"&gt;full Compose walkthrough at TechFuelHQ&lt;/a&gt; shows where to add the optional proxy network and GPU device.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>homelab</category>
      <category>linux</category>
    </item>
    <item>
      <title>LM Studio vs Ollama on an RTX 5080: The Settings Matter More Than the Wrapper</title>
      <dc:creator>LK Wood IV</dc:creator>
      <pubDate>Sat, 31 Oct 2026 12:01:50 +0000</pubDate>
      <link>https://dev.to/techfuelhq/lm-studio-vs-ollama-on-an-rtx-5080-the-settings-matter-more-than-the-wrapper-1kb6</link>
      <guid>https://dev.to/techfuelhq/lm-studio-vs-ollama-on-an-rtx-5080-the-settings-matter-more-than-the-wrapper-1kb6</guid>
      <description>&lt;p&gt;Adapted from &lt;a href="https://techfuelhq.com/self-hosted/ollama-vs-lm-studio-rtx-5080-2026/?utm_source=dev-to&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=distribution-2026-10&amp;amp;utm_content=lmstudio-ollama-settings" rel="noopener noreferrer"&gt;LM Studio vs Ollama on an RTX 5080&lt;/a&gt;, published on TechFuelHQ.&lt;/p&gt;

&lt;p&gt;If you are choosing between LM Studio and Ollama, choose by workflow, not by benchmark folklore.&lt;/p&gt;

&lt;p&gt;I tested both on the same RTX 5080, loading the same GGUF bytes wherever the formats allowed it. Ollama was modestly faster at dense-model decode. LM Studio was faster at prompt prefill. Neither wrapper produced the dramatic gap people often attribute to it.&lt;/p&gt;

&lt;p&gt;The largest performance differences came from memory settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Ollama&lt;/strong&gt; when the model is infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A background service&lt;/li&gt;
&lt;li&gt;An API for another application&lt;/li&gt;
&lt;li&gt;A Docker-based stack&lt;/li&gt;
&lt;li&gt;Something that should start with the machine and stay out of the way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;strong&gt;LM Studio&lt;/strong&gt; when the model is an interactive appliance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A graphical model browser&lt;/li&gt;
&lt;li&gt;A chat interface&lt;/li&gt;
&lt;li&gt;Per-model controls exposed in one place&lt;/li&gt;
&lt;li&gt;An OpenAI-compatible local server with visual configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On this test system, the applications were within 14% of one another under matched conditions. A single bad setting produced a 23% loss in LM Studio and a 3.4× slowdown in Ollama.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;Test system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RTX 5080 16 GB&lt;/li&gt;
&lt;li&gt;Windows 11&lt;/li&gt;
&lt;li&gt;NVIDIA driver 610.88&lt;/li&gt;
&lt;li&gt;Ollama 0.32.15&lt;/li&gt;
&lt;li&gt;LM Studio 0.4.21 with llama.cpp CUDA runtime 2.29.1&lt;/li&gt;
&lt;li&gt;Context length 4,096&lt;/li&gt;
&lt;li&gt;Full GPU offload&lt;/li&gt;
&lt;li&gt;Temperature 0&lt;/li&gt;
&lt;li&gt;Seed 42&lt;/li&gt;
&lt;li&gt;One serving slot&lt;/li&gt;
&lt;li&gt;Five fresh-prompt runs per decode cell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Llama 3.2 3B and Qwen 2.5 14B, both applications loaded identical, hash-verified GGUF files.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Application&lt;/th&gt;
&lt;th&gt;Decode&lt;/th&gt;
&lt;th&gt;Prompt prefill&lt;/th&gt;
&lt;th&gt;GPU memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B Q4_K_M&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;350.5 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17,862 tok/s&lt;/td&gt;
&lt;td&gt;3,760 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B Q4_K_M&lt;/td&gt;
&lt;td&gt;LM Studio&lt;/td&gt;
&lt;td&gt;316.0 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20,337 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3,808 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 14B Q4_K_M&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;102.2 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5,038 tok/s&lt;/td&gt;
&lt;td&gt;10,356 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 14B Q4_K_M&lt;/td&gt;
&lt;td&gt;LM Studio&lt;/td&gt;
&lt;td&gt;97.6 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,567 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10,405 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-oss 20B MXFP4&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;213.7 tok/s&lt;/td&gt;
&lt;td&gt;9,187 tok/s&lt;/td&gt;
&lt;td&gt;13,570 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-oss 20B MXFP4&lt;/td&gt;
&lt;td&gt;LM Studio&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;242.8 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9,618 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12,533 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ollama decoded the two dense models 5-11% faster. LM Studio prefilled every tested model 5-14% faster.&lt;/p&gt;

&lt;p&gt;The gpt-oss row is not an identical-file comparison. Ollama ships its own &lt;code&gt;gptoss&lt;/code&gt; conversion; LM Studio used the upstream ggml-org MXFP4 GGUF because its runtime rejected Ollama’s architecture-tagged blob. Engine and conversion differences both contribute to that result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both applications are packaging the same underlying server
&lt;/h2&gt;

&lt;p&gt;The process captures showed &lt;code&gt;llama-server.exe&lt;/code&gt; serving the model on both sides. Ollama runs its vendored build; LM Studio downloads its own llama.cpp runtime.&lt;/p&gt;

&lt;p&gt;That common engine explains why the dense-model results land close together. The wrapper affects defaults, model management, memory behavior, and workflow more than it changes the fundamental inference path.&lt;/p&gt;

&lt;p&gt;Versions still matter. Ollama’s Llama 3.2 3B result on this machine moved from roughly 319 tok/s to 351 tok/s across fourteen patch releases and six days. A local-LLM benchmark without application and runtime versions is a snapshot without a timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  LM Studio’s automatic offload cost 23%
&lt;/h2&gt;

&lt;p&gt;LM Studio initially ran gpt-oss 20B at about 186 tok/s with its stock automatic GPU-offload setting.&lt;/p&gt;

&lt;p&gt;I first normalized context length, slot count, and offload together, which raised the result to roughly 243 tok/s. To isolate the cause, I repeated the stock configuration and changed only the offload slider.&lt;/p&gt;

&lt;p&gt;The result was 244.9 tok/s.&lt;/p&gt;

&lt;p&gt;Automatic offload had held part of the model on the CPU even though the card could run the tested configuration fully on the GPU. LM Studio did not surface a warning. Moving one slider recovered the entire gap.&lt;/p&gt;

&lt;p&gt;Before comparing applications, confirm that LM Studio has not silently chosen partial CPU offload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ollama’s context setting caused a 3.4× slowdown
&lt;/h2&gt;

&lt;p&gt;The same class of failure appeared in Ollama.&lt;/p&gt;

&lt;p&gt;At a 4,096-token context, Llama 3.2 3B stayed fully GPU-resident and decoded at about 351 tok/s. This machine still had Ollama’s application-level context slider set to 256k from earlier work. Ollama clamped that request to the model’s 131,072-token maximum.&lt;/p&gt;

&lt;p&gt;The resulting footprint grew to about 18 GB and split across system RAM and VRAM. Decode fell to 102 tok/s.&lt;/p&gt;

&lt;p&gt;Nothing about the model or application version changed. The oversized KV cache alone turned a 351 tok/s result into a 102 tok/s result.&lt;/p&gt;

&lt;p&gt;If Ollama is unexpectedly slow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the configured context length.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;ollama ps&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that the model and KV cache are not spilling into system RAM.&lt;/li&gt;
&lt;li&gt;Retest with the context length the workload actually needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  VRAM was effectively a tie; system RAM was not
&lt;/h2&gt;

&lt;p&gt;Where both applications loaded the same file, GPU-memory use differed by less than 50 MiB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3,760 vs. 3,808 MiB for Llama 3.2 3B&lt;/li&gt;
&lt;li&gt;10,356 vs. 10,405 MiB for Qwen 2.5 14B&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;System RAM showed a larger difference.&lt;/p&gt;

&lt;p&gt;With no model loaded, the Ollama service tree used 78-132 MB across the quiet-window observations. LM Studio’s GUI and server used 709-928 MB.&lt;/p&gt;

&lt;p&gt;With gpt-oss resident, LM Studio’s process tree reported about 13 GB of system RAM alongside its 12.5 GB VRAM allocation. Ollama’s runner sat around 2.1 GB. Much of LM Studio’s total was memory-mapped file cache and therefore evictable, but it still matters on a machine with limited RAM or other services running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model files can be shared
&lt;/h2&gt;

&lt;p&gt;LM Studio imported the Ollama blobs for Llama 3.2 3B and Qwen 2.5 14B directly. Using the hard-link option let both applications name the same physical bytes on disk. Hash checks confirmed that the imported files matched Ollama’s stored blobs.&lt;/p&gt;

&lt;p&gt;The exception was gpt-oss 20B. Ollama’s blob declares the &lt;code&gt;gptoss&lt;/code&gt; architecture used by its fork, while LM Studio’s upstream llama.cpp runtime rejected it as unknown. That model required the upstream ggml-org GGUF on the LM Studio side.&lt;/p&gt;

&lt;p&gt;For most GGUF models, running both applications does not require maintaining two full weight libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which should you install?
&lt;/h2&gt;

&lt;p&gt;Choose Ollama if you want a lightweight service with simple API and container integration. Its dense-model decode edge was real on this system, and its idle RAM footprint was much smaller.&lt;/p&gt;

&lt;p&gt;Choose LM Studio if you want visual model discovery, a chat interface, and granular per-model controls. Its prompt-prefill performance was consistently better here, and the GUI makes configuration easier to inspect, provided you verify the automatic offload decision.&lt;/p&gt;

&lt;p&gt;Install both if your work genuinely uses both modes. They coexisted on this card on ports 11434 and 1234, and two resident 3B models used 7,000 MiB of total GPU memory including the desktop baseline.&lt;/p&gt;

&lt;p&gt;The actionable result is not that one wrapper is universally faster. It is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application gap was at most 14%. A wrong memory setting was worth up to 240%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check GPU offload. Check context length. Record the application and runtime versions. Then benchmark the workflow you actually run.&lt;/p&gt;

&lt;p&gt;The complete methodology, raw-result links, RAM observations, model hashes, limitations and the same-GPU comparison dataset are in the full LM Studio vs. Ollama comparison linked at the top.&lt;/p&gt;

</description>
      <category>localai</category>
      <category>ollama</category>
      <category>llm</category>
      <category>gpu</category>
    </item>
    <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: This article is adapted 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;. 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.&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>
