<?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: Schiff Heimlich</title>
    <description>The latest articles on DEV Community by Schiff Heimlich (@schiff_heimlich).</description>
    <link>https://dev.to/schiff_heimlich</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%2F3949704%2F89c08e96-274f-4f09-a299-8ebdabdc7096.jpg</url>
      <title>DEV Community: Schiff Heimlich</title>
      <link>https://dev.to/schiff_heimlich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/schiff_heimlich"/>
    <language>en</language>
    <item>
      <title>Bare-Metal Kubernetes: What NKP Metal Gets Right</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 25 Jul 2026 17:04:49 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-nkp-metal-gets-right-28fm</link>
      <guid>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-nkp-metal-gets-right-28fm</guid>
      <description>&lt;h1&gt;
  
  
  Bare-Metal Kubernetes: What NKP Metal Gets Right
&lt;/h1&gt;

&lt;p&gt;Here's something I ran into that might be worth a second look.&lt;/p&gt;

&lt;p&gt;Nutanix launched NKP Metal recently - basically their Kubernetes Platform extended to run directly on physical servers. I think the interesting part isn't the bare-metal capability itself, but what it means for operational consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical bit
&lt;/h2&gt;

&lt;p&gt;If you're running AI/ML workloads or dealing with edge deployments, bare-metal matters for one reason: you avoid the hypervisor layer. GPU passthrough works cleaner when there's nothing between the hardware and your workload. No hypervisor means no resource contention from the host OS stealing CPU cycles or memory.&lt;/p&gt;

&lt;p&gt;The other thing worth noting is the unified management angle. Same control plane whether you're running on VMs or physical hardware. That means one less management interface to switch between, one less place to check when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this actually helps
&lt;/h2&gt;

&lt;p&gt;This isn't a "bare-metal is always better" take. For most workloads, VMs are still the right answer - easier to snapshot, migrate, resize.&lt;/p&gt;

&lt;p&gt;But if you're doing GPU-heavy inference at the edge, or you've got hardware constraints that make virtualization impractical, bare-metal Kubernetes becomes relevant. And if you're already in the Nutanix ecosystem, having both under the same console means less context-switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The interesting part of NKP Metal isn't the bare-metal capability in isolation. It's the operational consistency - same API, same tooling, same management plane whether the underlying host is a VM or a physical server. That's the practical value for teams already running Nutanix.&lt;/p&gt;

&lt;p&gt;If you're evaluating Kubernetes infrastructure options and have GPU workloads that need direct hardware access, it's worth a look. If your workloads fit comfortably in VMs, this doesn't change much.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who pays attention to operational simplicity&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>systemd RestartSec does not wait for your process to actually exit</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Fri, 24 Jul 2026 17:03:53 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/systemd-restartsec-does-not-wait-for-your-process-to-actually-exit-4pl8</link>
      <guid>https://dev.to/schiff_heimlich/systemd-restartsec-does-not-wait-for-your-process-to-actually-exit-4pl8</guid>
      <description>&lt;p&gt;If you have ever set RestartSec on a systemd service and wondered why you still get Address already in use errors on restart, here is why.&lt;/p&gt;

&lt;p&gt;When RestartSec triggers a restart, systemd changes the PID but does not wait for the old process to fully release its resources. The new process starts, gets a new PID, but the old process is still holding port 443 or whatever for a few hundred milliseconds.&lt;/p&gt;

&lt;p&gt;The fix is straightforward. Use Type=oneshot which tells systemd to treat the service as a one-shot job. It will not try to manage the process lifecycle the same way, and subsequent restarts wait for the previous invocation to fully clean up.&lt;/p&gt;

&lt;p&gt;Or add a small delay with ExecStartPre=/bin/sleep 1. One second is usually enough.&lt;/p&gt;

&lt;p&gt;This bit me on a reverse proxy service that kept failing to bind 443 on restarts. The logs looked clean - new PID, service started - but connections were being refused. Took a while to realize the old process was still sitting on the port.&lt;/p&gt;

&lt;p&gt;Source: systemd.service 5 manpage - specifically the RestartSec behavior around process exit synchronization.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>Bare-Metal Kubernetes Without the Management Overhead</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:04:32 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bare-metal-kubernetes-without-the-management-overhead-572p</link>
      <guid>https://dev.to/schiff_heimlich/bare-metal-kubernetes-without-the-management-overhead-572p</guid>
      <description>&lt;h1&gt;
  
  
  Bare-Metal Kubernetes Without the Management Overhead
&lt;/h1&gt;

&lt;p&gt;Nutanix extended their Kubernetes Platform to bare-metal last week. It's called NKP Metal, and if you're running AI/ML workloads or edge deployments, this is worth knowing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical problem
&lt;/h2&gt;

&lt;p&gt;When you mix VMs and bare-metal in an infrastructure, you usually end up with separate management planes. Your virtualization team lives in vCenter or Nutanix AHV. Your Kubernetes clusters run somewhere else—maybe dedicated hardware, maybe in the cloud. Keeping inventory, networking, and policies in sync across both becomes a second job.&lt;/p&gt;

&lt;p&gt;NKP Metal attempts to solve this by putting bare-metal workers under the same Nutanix management layer you're already using for VMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually means for teams
&lt;/h2&gt;

&lt;p&gt;If you're already on Nutanix HCI, you can now extend the same console to physical Kubernetes workers. No separate hardware management domain. The cluster provisioning and lifecycle tooling stays consistent whether you're deploying a VM-based development cluster or a bare-metal production cluster for GPU workloads.&lt;/p&gt;

&lt;p&gt;The edge case here is straightforward: organizations running AI/ML inference at the edge (retail, manufacturing, telco) often need bare-metal for GPU access without the hypervisor overhead. Previously this meant separate tooling. NKP Metal collapses that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth evaluating if:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You're already on Nutanix HCI and evaluating Kubernetes placement options&lt;/li&gt;
&lt;li&gt;You have GPU workloads that need physical hosts but want unified operations&lt;/li&gt;
&lt;li&gt;You're tired of maintaining separate management workflows for VMs vs. containers on bare-metal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not a revolutionary change, but a practical one if you're already in that ecosystem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cover image: Unsplash&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>The BusyBox in Your Alpine Containers Is a Security Risk You Probably Didn't Know About</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:07:09 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/the-busybox-in-your-alpine-containers-is-a-security-risk-you-probably-didnt-know-about-3cjb</link>
      <guid>https://dev.to/schiff_heimlich/the-busybox-in-your-alpine-containers-is-a-security-risk-you-probably-didnt-know-about-3cjb</guid>
      <description>&lt;p&gt;Here's something I ran into that might be worth a second look.&lt;/p&gt;

&lt;p&gt;If you're running Docker containers based on Alpine Linux, you have BusyBox in your image. Most teams do, and most teams don't think about it twice. That might be a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue
&lt;/h2&gt;

&lt;p&gt;BusyBox was designed for embedded systems — small, resource-constrained environments where you need a bunch of Unix utilities in a single binary. It wasn't designed for cloud production workloads where security matters.&lt;/p&gt;

&lt;p&gt;The catch: when BusyBox has a vulnerability, your entire userspace is exposed. Unlike a traditional Linux distribution where each utility is a separate package with its own update cycle, BusyBox bundles everything into one binary. One CVE, and potentially every utility it provides is affected simultaneously.&lt;/p&gt;

&lt;p&gt;Alpine Linux uses BusyBox as its init system and provides the core userland utilities through it. That's efficient from a size perspective — Alpine images are small because of this. But it means your attack surface is concentrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Check
&lt;/h2&gt;

&lt;p&gt;Look at what's actually in your running containers:&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;# Check if BusyBox is present&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;your-container which busybox

&lt;span class="c"&gt;# See what version&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;your-container busybox | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building from alpine:latest or a similar base, BusyBox is there. It's not automatically a problem — but it does mean you need to track Alpine security announcements more closely than you might for other distros.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Build-Time Alternative
&lt;/h2&gt;

&lt;p&gt;There's work happening in the container security space around shifting validation to build time rather than runtime. The idea is that instead of scanning running containers for vulnerabilities, you validate container composition at build time and make security decisions then.&lt;/p&gt;

&lt;p&gt;For BusyBox specifically, this means checking whether your base image is tracking CVEs promptly, and whether your application actually needs everything BusyBox provides. If you're only using a subset of the utilities, you might be able to swap to a different base that provides those utilities as separate, independently-updateable packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Matters More
&lt;/h2&gt;

&lt;p&gt;The risk profile changes depending on your exposure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internet-facing containers&lt;/strong&gt;: Higher priority to track and update&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived ephemeral containers&lt;/strong&gt;: Still matters, but rotation helps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privilege level&lt;/strong&gt;: Containers running as root or with CAP_SYS_ADMIN need more attention&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;This isn't a reason to panic or rip out your Alpine-based images. Alpine is maintained by a competent team and they track security issues. But it's worth knowing what's in your containers, and having a process to update base images when BusyBox CVEs drop — because they do drop.&lt;/p&gt;

&lt;p&gt;A quick audit of which containers are running what base images, and a check on how automated your base image updates are, is probably worth 20 minutes of your time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Image: The Linux kernel provides the foundation, but your container's userland is equally important to keep patched&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Gitea's Docker Image Shipped a Dangerous Default, and It's Still Catching People</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:05:56 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/giteas-docker-image-shipped-a-dangerous-default-and-its-still-catching-people-1di0</link>
      <guid>https://dev.to/schiff_heimlich/giteas-docker-image-shipped-a-dangerous-default-and-its-still-catching-people-1di0</guid>
      <description>&lt;h1&gt;
  
  
  Gitea's Docker Image Shipped a Dangerous Default, and It's Still Catching People
&lt;/h1&gt;

&lt;p&gt;Gitea &amp;lt;=1.26.2 had a problem. The official Docker image set &lt;code&gt;REVERSE_PROXY_TRUSTED_PROXIES=*&lt;/code&gt; by default, which means it trusted the &lt;code&gt;X-WEBAUTH-USER&lt;/code&gt; header from any source. Ship it, run &lt;code&gt;docker compose up&lt;/code&gt;, and your private repos were accessible to anyone who sent the right header.&lt;/p&gt;

&lt;p&gt;CVE-2026-20896, CVSS 9.8. Caught in the wild within days of the PoC dropping.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the actual vulnerability looks like
&lt;/h2&gt;

&lt;p&gt;When Gitea runs behind a reverse proxy, it uses the &lt;code&gt;X-WEBAUTH-USER&lt;/code&gt; header to identify the authenticated user — if your proxy passes it through. With &lt;code&gt;REVERSE_PROXY_TRUSTED_PROXIES=*&lt;/code&gt;, Gitea accepts that header from any IP, not just your proxy. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-WEBAUTH-USER: admin"&lt;/span&gt; https://your-gitea.example.com/api/v1/repos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your Gitea user is actually named &lt;code&gt;admin&lt;/code&gt; — and a lot of them are — you just got read access to their private repos, no password needed.&lt;/p&gt;

&lt;p&gt;That's it. No zero-day exploit chain, no stolen credentials. Just a header.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check if you're exposed
&lt;/h2&gt;

&lt;p&gt;Look at how you start Gitea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose users:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITEA__server__PROXY_MODE=true&lt;/span&gt;
  &lt;span class="c1"&gt;# Check if you have REVERSE_PROXY_TRUSTED_PROXIES set to *&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker run:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; gitea/gitea:1.26
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;*&lt;/code&gt; anywhere in that config, you're trusting every IP that can reach your Gitea instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second check:&lt;/strong&gt; Who can actually reach your Gitea? If it's directly exposed to the internet (not behind a proper firewall or VPN-only access), that's the real problem. A wildcard proxy trust config on a publicly accessible instance is exactly as bad as it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Specify the actual IPs that are your reverse proxies. If your setup is a single Docker host with a reverse proxy on the same machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GITEA__server__PROXY_MODE=true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REVERSE_PROXY_TRUSTED_PROXIES=127.0.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if your proxy is on a private network segment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REVERSE_PROXY_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also set this in &lt;code&gt;app.ini&lt;/code&gt; directly under &lt;code&gt;[server]&lt;/code&gt; if you prefer config files over environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real issue
&lt;/h2&gt;

&lt;p&gt;This isn't a Gitea bug — the code works correctly given proper configuration. It's a shipped default that made sense in a narrow self-hosted scenario (single host, no exposure) but catches anyone who deploys "as documented" without auditing the security implications.&lt;/p&gt;

&lt;p&gt;The same class of issue shows up elsewhere: default credentials, open S3 buckets, debug endpoints on production. You're usually safe if your threat model includes "nobody tries to hit my services except through normal paths." That model breaks the moment something is internet-facing.&lt;/p&gt;

&lt;p&gt;If you're running Gitea from the official Docker image, check your proxy trust config. It's a two-minute audit that might save you from an incident.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who has stopped being surprised by shipped defaults&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>security</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Linux cgroups: Limiting Process Resources Without the Pain</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 20 Jul 2026 17:04:32 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/linux-cgroups-limiting-process-resources-without-the-pain-5f4e</link>
      <guid>https://dev.to/schiff_heimlich/linux-cgroups-limiting-process-resources-without-the-pain-5f4e</guid>
      <description>&lt;h1&gt;
  
  
  Linux cgroups: Limiting Process Resources Without the Pain
&lt;/h1&gt;

&lt;p&gt;Let me share something I ran into last week that might save you a headache.&lt;/p&gt;

&lt;p&gt;Had a script that was eating too much memory and killing adjacent services. The fix was simpler than I expected — &lt;code&gt;systemd-run&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quick Fix
&lt;/h2&gt;

&lt;p&gt;Instead of chasing down every poorly-written script and adding manual resource limits, you can just run the command with resource constraints upfront:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-run &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;MemoryLimit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256M your-script.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The script gets its own scope with a 256MB memory cap. When it tries to allocate more, the OOM killer handles it gracefully instead of taking down the whole machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Handy
&lt;/h2&gt;

&lt;p&gt;The thing I like about &lt;code&gt;systemd-run&lt;/code&gt; is that you don't need to edit service files or reboot. It's just a wrapper around the cgroups interface that systemd already manages.&lt;/p&gt;

&lt;p&gt;If you want persistent limits — like for a service that should always have constraints — you edit the unit file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;512M&lt;/span&gt;
&lt;span class="py"&gt;CPUQuota&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;50%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Couple of Flags Worth Knowing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MemoryHigh&lt;/code&gt; — this is the threshold where the kernel starts reclaiming memory aggressively. Useful if you want to warn before hitting the hard limit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CPUQuota&lt;/code&gt; — takes a percentage. &lt;code&gt;CPUQuota=50%&lt;/code&gt; means the service never gets more than half a CPU core, even if idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cgroupfs Path (If You Need It)
&lt;/h2&gt;

&lt;p&gt;For debugging, you can see what's actually happening:&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;cat&lt;/span&gt; /sys/fs/cgroup/systemd/system.slice/your-service.scope/memory.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each scope gets its own cgroup. You can read the limits, see current usage, and poke around without touching anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Reach for This
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scripts that call out to third-party binaries I don't trust&lt;/li&gt;
&lt;li&gt;One-off batch jobs that might go sideways&lt;/li&gt;
&lt;li&gt;Isolating services on shared homelab hardware&lt;/li&gt;
&lt;li&gt;Testing how software behaves under memory pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a silver bullet, but it's one of those tools that's cleaner than the alternatives I used to use (ulimit, nice, cgroups manually via &lt;code&gt;/sys/fs/cgroup/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Give &lt;code&gt;systemd-run&lt;/code&gt; a shot next time you need to contain something. The manual pages are actually decent on this one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Image: cgroups provide a hierarchical structure for resource control on Linux&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Nginx Log Woes: When $remote_addr Lies to You</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Mon, 20 Jul 2026 04:12:16 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/nginx-log-woes-when-remoteaddr-lies-to-you-4ld9</link>
      <guid>https://dev.to/schiff_heimlich/nginx-log-woes-when-remoteaddr-lies-to-you-4ld9</guid>
      <description>&lt;h1&gt;
  
  
  Nginx Log Woes: When &lt;code&gt;$remote_addr&lt;/code&gt; Lies to You
&lt;/h1&gt;

&lt;p&gt;Here's a fun one that bit me last week.&lt;/p&gt;

&lt;p&gt;You're running Nginx behind a reverse proxy or load balancer. You want to log the actual client IP for rate limiting. You check your logs and see... &lt;code&gt;127.0.0.1&lt;/code&gt; for every single request. Your rate limiter is blocking localhost. Not ideal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When a request hits Nginx through a proxy, &lt;code&gt;$remote_addr&lt;/code&gt; contains the proxy's IP, not the client's. Your config probably has something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt; &lt;span class="s"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/access.log&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;$remote_addr&lt;/code&gt; is your proxy. The real client IP is buried in a header.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Your proxy should be forwarding the real client IP via &lt;code&gt;X-Forwarded-For&lt;/code&gt; or &lt;code&gt;X-Real-IP&lt;/code&gt;. Then in Nginx you use the right variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$http_x_real_ip&lt;/span&gt; &lt;span class="s"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you trust the &lt;code&gt;X-Forwarded-For&lt;/code&gt; chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$http_x_forwarded_for&lt;/span&gt; &lt;span class="s"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But be careful with &lt;code&gt;X-Forwarded-For&lt;/code&gt; — it's a comma-separated list and can be spoofed if your proxy doesn't sanitize it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rate Limiting Piece
&lt;/h2&gt;

&lt;p&gt;For rate limiting, you need the actual client IP too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;real_ip_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# your proxy CIDR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Nginx to trust that header from your known proxy range and rewrite &lt;code&gt;$remote_addr&lt;/code&gt; accordingly.&lt;/p&gt;

&lt;p&gt;After reloading, check your logs. You should see real IPs now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Debug
&lt;/h2&gt;

&lt;p&gt;If it's not working:&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;# See what Nginx actually sees&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/nginx/access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare what hits your upstream directly vs through the proxy. One of those headers should populate.&lt;/p&gt;




&lt;p&gt;This bites teams regularly when they first set up a reverse proxy. The logs look fine until you need to debug or rate-limit, and then you're wondering why everyone's coming from the same IP. Happened to a client last month — their Cloudflare setup wasn't passing &lt;code&gt;CF-Connecting-IP&lt;/code&gt;, so rate limiting was a no-op.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>git absorb: the fixup workflow that sorts itself out</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:04:05 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/git-absorb-the-fixup-workflow-that-sorts-itself-out-1ljm</link>
      <guid>https://dev.to/schiff_heimlich/git-absorb-the-fixup-workflow-that-sorts-itself-out-1ljm</guid>
      <description>&lt;p&gt;Here's a small thing that has made my git workflow less tedious.&lt;/p&gt;

&lt;p&gt;When you're working on a feature branch and get review feedback, you usually end up doing this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make your fixes&lt;/li&gt;
&lt;li&gt;Find the commit SHA that needs the fix&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit --fixup &amp;lt;sha&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git rebase -i --autosquash&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is the annoying part. You're scanning &lt;code&gt;git log&lt;/code&gt;, copying the SHA, maybe getting it wrong.&lt;/p&gt;

&lt;p&gt;git-absorb automates the bookkeeping. You stage your files, run &lt;code&gt;git absorb&lt;/code&gt;, and it figures out which commits your changes belong to and creates the fixup commits for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nv"&gt;$FILES_YOU_FIXED&lt;/span&gt;
git absorb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you trust it, &lt;code&gt;--and-rebase&lt;/code&gt; squishes everything in one go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nv"&gt;$FILES_YOU_FIXED&lt;/span&gt;
git absorb &lt;span class="nt"&gt;--and-rebase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to check first, just run &lt;code&gt;git absorb&lt;/code&gt; without the flag, look at what it generated with &lt;code&gt;git log&lt;/code&gt;, then run &lt;code&gt;git rebase -i --autosquash&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;It's a Rust port of hg absorb, built by Facebook. Install it from the releases page or via cargo.&lt;/p&gt;

&lt;p&gt;The workflow it enables is clean: make your changes, stage the files, let the tool sort out which commit gets what. No SHA hunting.&lt;/p&gt;

&lt;p&gt;If you're still doing fixups manually, give it a try.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>git</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>When Your Scheduled Job Takes Longer Than Its Interval</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Tue, 14 Jul 2026 17:05:40 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/when-your-scheduled-job-takes-longer-than-its-interval-kok</link>
      <guid>https://dev.to/schiff_heimlich/when-your-scheduled-job-takes-longer-than-its-interval-kok</guid>
      <description>&lt;p&gt;Had an interesting realization about job queues this week that I figured I would share since it came up in a code review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You are running a scheduled job. It is configured to run every N hours. Most of the time it finishes in time, but sometimes it does not — maybe it hits a rate limit, maybe the data volume is higher than usual, whatever.&lt;/p&gt;

&lt;p&gt;What happens when your job is still running when the scheduler tries to start it again?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The semantics you probably have not thought about&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turns out, job queue implementations typically give you a few options when this happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer New&lt;/strong&gt;: Cancel the running job, start the new one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer Old&lt;/strong&gt;: Let the running job finish, skip the new trigger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt;: Queue the new trigger, run it after the current one finishes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel&lt;/strong&gt;: Run both concurrently (if you have concurrency &amp;gt; 1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most people, including me apparently, assume Prefer New is the sensible default. Newer runs should use fresher data, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where that assumption breaks down&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the scenario that made me rethink this:&lt;/p&gt;

&lt;p&gt;You are running a job that takes 7 hours on weekends but only 2 hours on weekdays (less data to process). You set the interval to 3 hours thinking the 2-hour job will finish well before the next trigger.&lt;/p&gt;

&lt;p&gt;On the weekend, your 7-hour job starts. At the 3-hour mark, a new trigger fires. With Prefer New, you cancel the running job and start fresh. It gets canceled again at the 6-hour mark. And again. You will run the job 16 times over a weekend and none of them will ever finish.&lt;/p&gt;

&lt;p&gt;With Prefer Old, the running job just continues. You might cancel a few queued triggers, but your job actually completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical takeaway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you are configuring scheduled jobs, think about what should happen when the job outlives its interval. Prefer Old feels wrong intuitively, but in situations where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your job takes longer than expected due to external factors&lt;/li&gt;
&lt;li&gt;Stale results are better than no results&lt;/li&gt;
&lt;li&gt;You want to avoid wasted compute on canceled runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...it might actually be the right choice.&lt;/p&gt;

&lt;p&gt;Check what semantics your job queue exposes. Celery has task_acks_late and task_reject_on_worker_lost. Sidekiq has lock options. Bull, Kubernetes CronJobs — they all handle this differently.&lt;/p&gt;

&lt;p&gt;The defaults might not match what you actually need.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Bare-Metal Kubernetes: What the Noise Is Actually About</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sun, 12 Jul 2026 17:04:50 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-the-noise-is-actually-about-4i6</link>
      <guid>https://dev.to/schiff_heimlich/bare-metal-kubernetes-what-the-noise-is-actually-about-4i6</guid>
      <description>&lt;h1&gt;
  
  
  Bare-Metal Kubernetes: What the Noise Is Actually About
&lt;/h1&gt;

&lt;p&gt;You have probably seen the announcements. Nutanix launched NKP Metal. Several vendors are pushing bare-metal Kubernetes offerings. The press makes it sound like the next revolution.&lt;/p&gt;

&lt;p&gt;Here's what's actually going on, and why you should care.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Being Solved
&lt;/h2&gt;

&lt;p&gt;If you run Kubernetes in the cloud, you're abstractions deep: containers on VMs on hypervisors on physical hardware. Each layer adds latency, consumes overhead, and introduces failure modes.&lt;/p&gt;

&lt;p&gt;For most workloads, this doesn't matter. Your web app doesn't care if the hypervisor adds 0.3ms of latency.&lt;/p&gt;

&lt;p&gt;For a specific class of workloads, it does matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency-sensitive applications&lt;/strong&gt; at the edge — think content delivery, local inference, real-time processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-throughput data plane operations&lt;/strong&gt; — network functions, storage controllers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-direct workloads&lt;/strong&gt; — where you want the container talking directly to the hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The push toward bare-metal Kubernetes is about removing the VM layer for these specific cases. Not all Kubernetes. Just the parts where the hypervisor tax actually costs you something.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changes When You Remove the VM Layer
&lt;/h2&gt;

&lt;p&gt;Running Kubernetes directly on physical nodes changes a few things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No more VM overhead.&lt;/strong&gt; No KVM/QEMU tax. Your pod gets the full CPU, memory, and I/O of the physical host. For a GPU workload, this means direct PCI-e access without the virtualization layer introducing latency jitter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplified resource allocation.&lt;/strong&gt; You don't have to think about VM sizes and node counts separately. The node &lt;em&gt;is&lt;/em&gt; the physical host. This sounds simpler but requires different operational thinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New operational requirements.&lt;/strong&gt; No live migration. No VM-level snapshots. If a physical node fails, pods don't get rescheduled automatically the way they do with VMs. You need different health-check and recovery strategies.&lt;/p&gt;

&lt;p&gt;Here's what that actually looks like operationally:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`bash&lt;/p&gt;

&lt;h1&gt;
  
  
  With VMs, a node failure triggers live migration or restart
&lt;/h1&gt;

&lt;h1&gt;
  
  
  With bare metal, you're dealing with hardware
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Health checking needs to be tighter
&lt;/h1&gt;

&lt;p&gt;apiVersion: apps/v1&lt;br&gt;
kind: Deployment&lt;br&gt;
metadata:&lt;br&gt;
  name: edge-workload&lt;br&gt;
spec:&lt;br&gt;
  replicas: 3&lt;br&gt;
  template:&lt;br&gt;
    spec:&lt;br&gt;
      topologySpreadConstraints:&lt;br&gt;
      - maxSkew: 1&lt;br&gt;
        topologyKey: kubernetes.io/hostname&lt;br&gt;
        whenUnsatisfiable: DoNotSchedule&lt;br&gt;
      # Force spread across physical hosts, not VMs&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The constraint &lt;code&gt;whenUnsatisfiable: DoNotSchedule&lt;/code&gt; matters more on bare metal. You don't want two replicas landing on the same host if that host is your only option for that hardware profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical Difference for Edge Deployments
&lt;/h2&gt;

&lt;p&gt;Edge Kubernetes has a different failure profile than cloud. In cloud, a node failure is common, recoverable in seconds. At the edge, a node failure might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Physical access required&lt;/li&gt;
&lt;li&gt;Remote location with limited connectivity&lt;/li&gt;
&lt;li&gt;Single-node "clusters" because hardware is expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bare-metal Kubernetes at the edge means you're thinking about:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`yaml&lt;/p&gt;

&lt;h1&gt;
  
  
  Edge cluster with local storage persistence
&lt;/h1&gt;

&lt;p&gt;apiVersion: storage.k8s.io/v1&lt;br&gt;
kind: StorageClass&lt;br&gt;
metadata:&lt;br&gt;
  name: local-storage&lt;br&gt;
provisioner: kubernetes.io/no-provisioner&lt;br&gt;
volumeBindingMode: WaitForFirstConsumer&lt;/p&gt;




&lt;h1&gt;
  
  
  Node affinity to keep stateful workloads pinned
&lt;/h1&gt;

&lt;p&gt;apiVersion: apps/v1&lt;br&gt;
kind: StatefulSet&lt;br&gt;
metadata:&lt;br&gt;
  name: edge-db&lt;br&gt;
spec:&lt;br&gt;
  serviceName: "edge-db"&lt;br&gt;
  replicas: 1  # Sometimes you only have one node&lt;br&gt;
  selector:&lt;br&gt;
    matchLabels:&lt;br&gt;
      app: edge-db&lt;br&gt;
  template:&lt;br&gt;
    spec:&lt;br&gt;
      affinity:&lt;br&gt;
        nodeAffinity:&lt;br&gt;
          requiredDuringSchedulingIgnoredDuringExecution:&lt;br&gt;
            nodeSelectorTerms:&lt;br&gt;
            - matchExpressions:&lt;br&gt;
              - key: node-type&lt;br&gt;
                operator: In&lt;br&gt;
                values:&lt;br&gt;
                - compute-node&lt;br&gt;
      tolerations:&lt;br&gt;
      - key: "node.kubernetes.io/unreachable"&lt;br&gt;
        operator: "Exists"&lt;br&gt;
        effect: "NoExecute"&lt;br&gt;
        tolerationSeconds: 300&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tolerationSeconds: 300&lt;/code&gt; buys you 5 minutes before pods get evicted when a node becomes unreachable. At the edge, you want this grace period — you might be dealing with a temporary network partition, not an actual node failure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This Makes Sense
&lt;/h2&gt;

&lt;p&gt;Bare-metal Kubernetes is not a replacement for cloud Kubernetes. It's a specific tool for specific scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Makes sense:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge locations with latency-sensitive workloads&lt;/li&gt;
&lt;li&gt;Telco/CNF workloads requiring low jitter&lt;/li&gt;
&lt;li&gt;GPU clusters where you want direct PCI-e access&lt;/li&gt;
&lt;li&gt;Remote locations where compute is expensive and you need efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Does not make sense:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General web applications&lt;/li&gt;
&lt;li&gt;Development/test environments&lt;/li&gt;
&lt;li&gt;Workloads that scale horizontally in cloud&lt;/li&gt;
&lt;li&gt;Teams without operational experience managing physical infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Nutanix move is interesting because they span both worlds — VMs for general workloads, bare metal for specialized ones. Unified management across both is the actual value proposition for organizations already in that ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Operational Reality Check
&lt;/h2&gt;

&lt;p&gt;Before jumping on this, understand what you're signing up for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No live migration means planned maintenance is different.&lt;/strong&gt; Upgrading the kernel on a bare-metal node means your pods go down. You need proper PodDisruptionBudgets and graceful draining:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;`bash&lt;/p&gt;

&lt;h1&gt;
  
  
  Check if your workload can tolerate node maintenance
&lt;/h1&gt;

&lt;p&gt;kubectl get poddisruptionbudgets -A&lt;br&gt;
kubectl describe poddisruptionbudgets &lt;/p&gt;

&lt;h1&gt;
  
  
  Drain before maintenance
&lt;/h1&gt;

&lt;p&gt;kubectl drain  --ignore-daemonsets --delete-emptydir-data&lt;br&gt;
`&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware failures are not transparent.&lt;/strong&gt; A bad DIMM, a failing SSD, a RAID controller glitch — these don't show up in &lt;code&gt;kubectl get nodes&lt;/code&gt; the same way a VM failure does. You need IPMI/BMC access and hardware monitoring out of band.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory management is physical.&lt;/strong&gt; Server serial numbers, firmware versions, BIOS settings — this is not &lt;code&gt;aws ec2 describe-instances&lt;/code&gt;. It's a spreadsheet at minimum, hardware tags and asset IDs at best.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Opportunity
&lt;/h2&gt;

&lt;p&gt;For most teams, cloud Kubernetes is fine. The hypervisor tax is real but small, and the operational simplicity is worth it.&lt;/p&gt;

&lt;p&gt;For teams running edge infrastructure, telco workloads, or specialized compute (GPU, FPGA, network accelerators), bare-metal Kubernetes solves a real problem. The latency and efficiency gains are measurable.&lt;/p&gt;

&lt;p&gt;The key is knowing which category your workload falls into. If you can't articulate why you need bare metal, you probably don't. And if you can, you're already aware of the operational tradeoffs.&lt;/p&gt;

&lt;p&gt;This isn't a revolution. It's a specialized deployment model getting better tooling support. That's worth paying attention to if you're in that space.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cloudnativenow.com/features/pedal-to-bare-metal-kubernetes-nutanix-forges-nkp-metal/" rel="noopener noreferrer"&gt;Pedal to Bare-Metal Kubernetes: Nutanix Forges NKP Metal&lt;/a&gt; — Cloud Native Now&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>jq in Shell Scripts: The Small Things That Trip You Up</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Sat, 11 Jul 2026 17:05:35 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/jq-in-shell-scripts-the-small-things-that-trip-you-up-3hh2</link>
      <guid>https://dev.to/schiff_heimlich/jq-in-shell-scripts-the-small-things-that-trip-you-up-3hh2</guid>
      <description>&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;If you work with JSON APIs, configs, or log processing, you have probably used &lt;code&gt;jq&lt;/code&gt; in a shell script. It is solid. But there are a few edge cases that trip people up regularly — things that work fine in tests but break in production.&lt;/p&gt;

&lt;p&gt;Here is what I have run into.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Null vs. Missing Keys
&lt;/h2&gt;

&lt;p&gt;Say you have JSON like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"running"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you want to get the &lt;code&gt;status&lt;/code&gt; field. Easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".status"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web01&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;span class="c"&gt;# returns: running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what if the field is missing or null?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web01"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; will return &lt;code&gt;null&lt;/code&gt;. Your script might handle that fine — or it might not. The problem comes when you try to do arithmetic or comparisons on null.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s2"&gt;".count + 1"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: null}"&lt;/span&gt;
&lt;span class="c"&gt;# returns: null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not helpful. You need to handle this explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".count // 0 + 1"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: null}"&lt;/span&gt;
&lt;span class="c"&gt;# returns: 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;//&lt;/code&gt; operator provides a default value when the left side is null or missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Piping Arrays Correctly
&lt;/h2&gt;

&lt;p&gt;Another one. You have an array of objects and want to filter them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want hosts with cpu &amp;gt; 50.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s2"&gt;".[] | select(.cpu &amp;gt; 50)"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web01&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 45}, {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web02&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 12}, {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web03&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 78}]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine, right? But now try to get just the hostnames as a list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s2"&gt;"[.[] | select(.cpu &amp;gt; 50) | .host]"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web01&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 45}, {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web02&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 12}, {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web03&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 78}]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"web03"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works. But if nothing matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s2"&gt;"[.[] | select(.cpu &amp;gt; 100) | .host]"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;web01&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 45}]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not null — an empty array. Your script might check for null and think "no data" when it is actually "zero results". Different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workaround
&lt;/h2&gt;

&lt;p&gt;When I write jq in shell scripts that handle production data, I usually wrap things defensively:&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;# Get a value with a default, even if key is missing&lt;/span&gt;
&lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;".cpu // &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Check if result is valid before proceeding&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"null"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No CPU data"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or I use &lt;code&gt;jq -e&lt;/code&gt; to check for null/empty results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".cpu"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cpu&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: null}"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 5 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Key not found"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Error code 5 means the output was null.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Point
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; is reliable. But when you mix shell scripting with JSON processing, you hit edge cases around null handling and empty results. A few defensive patterns save you from late-night debugging sessions.&lt;/p&gt;

&lt;p&gt;These are not jq bugs — they are just things to know when you are writing scripts that process real data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Schiff Heimlich | Sysadmin who learned this the hard way&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>shell</category>
      <category>bash</category>
      <category>jq</category>
    </item>
    <item>
      <title>Linux 7.2 cut pipe mutex contention — why your shell pipelines just got faster</title>
      <dc:creator>Schiff Heimlich</dc:creator>
      <pubDate>Wed, 01 Jul 2026 17:04:58 +0000</pubDate>
      <link>https://dev.to/schiff_heimlich/linux-72-cut-pipe-mutex-contention-why-your-shell-pipelines-just-got-faster-4hg6</link>
      <guid>https://dev.to/schiff_heimlich/linux-72-cut-pipe-mutex-contention-why-your-shell-pipelines-just-got-faster-4hg6</guid>
      <description>&lt;p&gt;A Meta engineer profiling caching code found something worth fixing in the kernel's pipe write path. The fix landed in Linux 7.2: &lt;code&gt;anon_pipe_write&lt;/code&gt; now pre-allocates up to 8 pages before grabbing the lock, cutting the critical section down significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Pipes use a ring buffer backed by anonymous pages. On write, the kernel would allocate pages under the same mutex used for the actual data copy — meaning every write had to wait for allocation, and allocation held the lock. Under load this caused measurable mutex contention.&lt;/p&gt;

&lt;p&gt;The fix separates allocation from the critical section. If 8 pages are already pre-allocated and available, the write just copies data and updates the ring buffer pointer — no allocation, no lock held during the slow path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;The gains depend on your workload. Meta's testing showed meaningful improvements under memory pressure — the kind of situation where page allocation itself becomes expensive. Under lighter load the difference is smaller, but still present since you're avoiding the allocation path entirely when the pre-allocated pages cover the write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for pipeline work
&lt;/h2&gt;

&lt;p&gt;If you run anything that pipes data through shell utilities — log processing, build artifact transforms, text munging, any &lt;code&gt;sort | uniq | awk&lt;/code&gt; chain — you benefit from this. Pipes are the fundamental I/O primitive underneath all of it. Reducing contention at this layer makes the whole chain a bit more predictable under concurrent load.&lt;/p&gt;

&lt;p&gt;You don't need to do anything. This lands in your kernel update. But it's worth knowing why those &lt;code&gt;cat bigfile | sort | head&lt;/code&gt; runs feel a touch snappier on a recent kernel — it's not just compiler optimizations, it's a genuine kernel path improvement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kernel 7.2 or later required. Check with &lt;code&gt;uname -r&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>linux</category>
      <category>news</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
