<?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: DevOps AI ToolKit</title>
    <description>The latest articles on DEV Community by DevOps AI ToolKit (devopsaitoolkit).</description>
    <link>https://dev.to/devopsaitoolkit</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%2Forganization%2Fprofile_image%2F13604%2F5a89fd4b-4719-4ab6-b098-ded19cf04e72.png</url>
      <title>DEV Community: DevOps AI ToolKit</title>
      <link>https://dev.to/devopsaitoolkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devopsaitoolkit"/>
    <language>en</language>
    <item>
      <title>The 12 DevOps Errors That Page Teams Most (And the First Thing to Check)</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:58:06 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/the-12-devops-errors-that-page-teams-most-and-the-first-thing-to-check-hll</link>
      <guid>https://dev.to/devopsaitoolkit/the-12-devops-errors-that-page-teams-most-and-the-first-thing-to-check-hll</guid>
      <description>&lt;p&gt;Over the last while I've been cataloguing production DevOps errors — the exact strings that show up in logs at 2 a.m. — and writing a fix for each one. A pattern jumps out fast: a small number of errors account for a huge share of the pages. Here are the twelve that come up most, with the one-thing-to-check-first for each.&lt;/p&gt;

&lt;p&gt;None of these are exotic. That's the point. The stuff that actually pages you is rarely exotic — it's the same dozen failure modes wearing different hats.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;CrashLoopBackOff&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The pod started, died, and Kubernetes is now backing off between restarts. &lt;code&gt;CrashLoopBackOff&lt;/code&gt; is a &lt;em&gt;symptom&lt;/em&gt;, never a cause. Go straight to &lt;code&gt;kubectl logs &amp;lt;pod&amp;gt; --previous&lt;/code&gt; — the logs from the crashed container are where the real error lives. Nine times out of ten it's a bad config value, a missing env var, or a failed migration on startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;ImagePullBackOff&lt;/code&gt; / &lt;code&gt;ErrImagePull&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Kubernetes can't pull the image. Don't guess — &lt;code&gt;kubectl describe pod&lt;/code&gt; spells it out in Events. It's almost always a typo in the tag, a missing &lt;code&gt;imagePullSecret&lt;/code&gt;, or a registry rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;OOMKilled&lt;/code&gt; (exit code 137)
&lt;/h2&gt;

&lt;p&gt;This is &lt;em&gt;not&lt;/em&gt; "the node ran out of memory." It's "this container hit &lt;strong&gt;its own&lt;/strong&gt; cgroup memory limit and the kernel killed it." Different problem, different fix. Compare the pod's &lt;code&gt;resources.limits.memory&lt;/code&gt; against what it actually uses (&lt;code&gt;kubectl top pod&lt;/code&gt;) before you touch anything at the node level.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;No space left on device&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The classic — and the trap is when &lt;code&gt;df -h&lt;/code&gt; shows free space anyway. Then it's one of two things: you're out of &lt;strong&gt;inodes&lt;/strong&gt; (&lt;code&gt;df -i&lt;/code&gt;), or a process is holding a &lt;strong&gt;deleted-but-still-open&lt;/strong&gt; file (&lt;code&gt;lsof +L1&lt;/code&gt;). &lt;code&gt;rm&lt;/code&gt; won't reclaim that space until you restart the process holding the file descriptor.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. DNS timeouts inside pods
&lt;/h2&gt;

&lt;p&gt;An external lookup that works from the node but intermittently times out inside a pod is almost always the &lt;code&gt;ndots:5&lt;/code&gt; search-domain cascade colliding with a conntrack UDP race — you get a flat 5-second stall that blows your client timeout. Overriding &lt;code&gt;ndots&lt;/code&gt; on the pod spec and running NodeLocal DNSCache is the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt; (Postgres)
&lt;/h2&gt;

&lt;p&gt;Bumping &lt;code&gt;max_connections&lt;/code&gt; is the trap, not the fix — each connection costs real memory. You need a pooler (PgBouncer), not 500 backend processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;Connection refused&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Something reached the host and nothing was listening on that port. It's rarely DNS or the network — it's the service being down, bound to &lt;code&gt;127.0.0.1&lt;/code&gt; instead of &lt;code&gt;0.0.0.0&lt;/code&gt;, or a firewall. &lt;code&gt;ss -tlnp&lt;/code&gt; on the target tells you in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;TLS handshake timeout&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Usually not a cert problem at all — it's a network path problem (MTU, a proxy, or a firewall silently dropping the handshake) masquerading as TLS. Test raw connectivity first with &lt;code&gt;openssl s_client -connect host:443&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. &lt;code&gt;Read-only file system&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A filesystem that was mounted read-write and is suddenly read-only almost always means the kernel remounted it &lt;code&gt;ro&lt;/code&gt; after detecting I/O errors. Check &lt;code&gt;dmesg&lt;/code&gt; — you may be looking at a failing disk, not a permissions issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;code&gt;Multi-Attach error for volume&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;ReadWriteOnce&lt;/code&gt; volume can attach to exactly one &lt;strong&gt;node&lt;/strong&gt; at a time — not one pod, one node. If a node goes &lt;code&gt;NotReady&lt;/code&gt; with the volume still attached, a pod rescheduled elsewhere gets this error. Kubernetes waits ~6 minutes before force-detaching &lt;em&gt;on purpose&lt;/em&gt; — to protect your data from being written by two hosts at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. &lt;code&gt;502 Bad Gateway&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;502 means your proxy reached the upstream and the upstream said no (or died). It's rarely the proxy. &lt;code&gt;connect() failed (111: Connection refused)&lt;/code&gt; in the NGINX error log → your app isn't listening where the proxy thinks it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. &lt;code&gt;exec format error&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You built an image for one CPU architecture and ran it on another (hello, Apple Silicon → x86 clusters). Build multi-arch, or match your &lt;code&gt;--platform&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Every one of these has the same shape: the error message describes the &lt;em&gt;symptom&lt;/em&gt; the system noticed, not the &lt;em&gt;cause&lt;/em&gt; you need to fix. &lt;code&gt;CrashLoopBackOff&lt;/code&gt; isn't why your pod is dying. &lt;code&gt;OOMKilled&lt;/code&gt; isn't the node. The skill isn't memorizing fixes — it's knowing which single command turns the symptom back into a cause.&lt;/p&gt;

&lt;p&gt;I keep a full, searchable library of these — every error above has a complete guide with the diagnostic workflow, an example root-cause analysis, and the prevention checklist. If you want the deeper version of any of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Kubernetes ones live in the &lt;a href="https://devopsaitoolkit.com/stacks/kubernetes/" rel="noopener noreferrer"&gt;Kubernetes troubleshooting toolkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Everything else is in the &lt;a href="https://devopsaitoolkit.com/blog/" rel="noopener noreferrer"&gt;full error-guide library&lt;/a&gt; (Linux, Postgres, Docker, NGINX, and more)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the error that pages &lt;em&gt;your&lt;/em&gt; team most? Curious whether it's on this list or something I should go write up next.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>sre</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built Free Browser-Based Validators for YAML, Kubernetes and Terraform (No Upload, No Signup)</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:32:55 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/i-built-free-browser-based-validators-for-yaml-kubernetes-and-terraform-no-upload-no-signup-57ka</link>
      <guid>https://dev.to/devopsaitoolkit/i-built-free-browser-based-validators-for-yaml-kubernetes-and-terraform-no-upload-no-signup-57ka</guid>
      <description>&lt;p&gt;Every DevOps engineer has done this dance: you've got a chunk of YAML or a Terraform file that &lt;em&gt;looks&lt;/em&gt; right, something's rejecting it, and you want a fast sanity check. So you paste it into some random online validator — and a small voice asks, &lt;em&gt;wait, where did that config just go?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That config often has structure, comments, sometimes internal hostnames or resource names in it. Pasting infrastructure definitions into an unknown server is a habit worth breaking. So I built a set of validators that never send your config anywhere — they run entirely in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they are
&lt;/h2&gt;

&lt;p&gt;Free, browser-based validators for the formats DevOps folks paste-and-pray most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YAML&lt;/strong&gt; — catches the indentation and structure errors that make Kubernetes and CI configs fail with cryptic messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes manifests&lt;/strong&gt; — schema-aware checks beyond "is it valid YAML," so you catch the wrong &lt;code&gt;apiVersion&lt;/code&gt; or a misplaced field before &lt;code&gt;kubectl apply&lt;/code&gt; does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform / HCL&lt;/strong&gt; — structural validation for the syntax slips that &lt;code&gt;terraform validate&lt;/code&gt; flags only after you've context-switched away&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one design decision that matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;100% client-side.&lt;/strong&gt; No upload, no signup, no server round-trip. Your config is parsed by JavaScript running in your own tab — it never leaves your machine. You can literally open dev-tools, watch the network panel, and see nothing go out. Turn off your wifi and they still work.&lt;/p&gt;

&lt;p&gt;This isn't a privacy gimmick — it's the correct architecture for a tool that handles infrastructure definitions. A validator has no business seeing your config on a server it doesn't need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I bother
&lt;/h2&gt;

&lt;p&gt;Two reasons, honestly.&lt;/p&gt;

&lt;p&gt;One: I kept wanting this exact thing and kept not trusting the options. The nth time I hesitated before pasting a manifest into a stranger's website, I decided to just build the version I'd trust.&lt;/p&gt;

&lt;p&gt;Two: fast feedback loops are the whole game in this job. The gap between "save the file" and "find out it's malformed" is pure friction — and the tighter that loop, the less of your working memory it burns. A validator that's one tab away and gives an answer in milliseconds is a small thing that compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://devopsaitoolkit.com/validators/" rel="noopener noreferrer"&gt;validator workbench&lt;/a&gt; — YAML, Kubernetes, and Terraform, all client-side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're the kind of person who'd rather script it, a lot of the underlying tooling is open source — CLIs and a small read-only API for the prompt and error-guide data — over on the &lt;a href="https://devopsaitoolkit.com/developers/" rel="noopener noreferrer"&gt;developer page&lt;/a&gt; and the &lt;a href="https://github.com/devopsaitoolkit" rel="noopener noreferrer"&gt;GitHub org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Client-side tools have real limits — they can't know your cluster's live state, and schema validation isn't the same as a policy check. But for the "did I just fat-finger the indentation" question, having the answer without a network request is exactly the trade I want.&lt;/p&gt;

&lt;p&gt;What config format do you most wish had a trustworthy, offline, no-signup validator? That's genuinely how I decide what to build next.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>yaml</category>
      <category>opensource</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>It Works on My Machine: A Docker War Story About exec format error</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Fri, 10 Jul 2026 14:45:41 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/it-works-on-my-machine-a-docker-war-story-about-exec-format-error-oen</link>
      <guid>https://dev.to/devopsaitoolkit/it-works-on-my-machine-a-docker-war-story-about-exec-format-error-oen</guid>
      <description>&lt;p&gt;"It works on my machine" is the oldest joke in software, and containers were supposed to kill it. Same image everywhere, same behavior everywhere — that's the whole pitch. So there's a special kind of betrayal when a container that runs perfectly on your laptop lands in the cluster and dies instantly with four unhelpful words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exec /app/server: exec format error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the afternoon that error cost me, and the thing it turned out to be teaching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Built the image locally on a shiny new laptop. Ran it locally — perfect. Pushed it, the deploy rolled out, and every pod went straight into &lt;code&gt;CrashLoopBackOff&lt;/code&gt;. &lt;code&gt;kubectl logs&lt;/code&gt; showed the line above and nothing else. No stack trace, no panic, no hint. The binary that ran fine thirty seconds ago on my machine refused to execute at all in prod.&lt;/p&gt;

&lt;p&gt;The maddening part, same as it always is: &lt;em&gt;the exact same image&lt;/em&gt;. That's the container promise. How can the same bytes run in one place and be unrunnable in another?&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell I walked right past
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;exec format error&lt;/code&gt; is the kernel's way of saying "I tried to execute this file and I don't recognize the format." Not "permission denied," not "not found" — &lt;em&gt;I literally cannot run this shape of binary.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the shape of a binary that a kernel can or can't run is its &lt;strong&gt;CPU architecture&lt;/strong&gt;. My shiny new laptop was Apple Silicon — &lt;code&gt;arm64&lt;/code&gt;. The cluster nodes were &lt;code&gt;amd64&lt;/code&gt;. I'd built an &lt;code&gt;arm64&lt;/code&gt; binary, wrapped it in an image, and shipped it to machines that speak a different instruction set. Locally it ran because I was running it &lt;em&gt;on the architecture I built it for.&lt;/em&gt; The moment it hit an &lt;code&gt;amd64&lt;/code&gt; node, the kernel looked at my &lt;code&gt;arm64&lt;/code&gt; executable and said, correctly, "I don't know how to run this."&lt;/p&gt;

&lt;p&gt;Nothing was broken. Docker did exactly what I asked — it built an image for the platform I was on and faithfully shipped it. I just never told it that "the platform I'm on" and "the platform this runs on" were different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming it
&lt;/h2&gt;

&lt;p&gt;Two commands make it obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# what architecture is this image built for?&lt;/span&gt;
docker image inspect myimage:tag &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Architecture}}'&lt;/span&gt;
&lt;span class="c"&gt;# arm64   ← there's the problem&lt;/span&gt;

&lt;span class="c"&gt;# what do the target nodes run?&lt;/span&gt;
kubectl get nodes &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[*].status.nodeInfo.architecture}'&lt;/span&gt;
&lt;span class="c"&gt;# amd64 amd64 amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;arm64&lt;/code&gt; image, &lt;code&gt;amd64&lt;/code&gt; nodes. Mystery over.&lt;/p&gt;

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

&lt;p&gt;Stop building for "wherever I happen to be" and start building for where it runs. &lt;code&gt;docker buildx&lt;/code&gt; builds multi-arch images from a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; registry/myimage:tag &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the registry holds both architectures under one tag, and every node pulls the variant it can actually run. If you only ever deploy to &lt;code&gt;amd64&lt;/code&gt;, you can just pin that: &lt;code&gt;--platform linux/amd64&lt;/code&gt;. Either way, the key is that the build platform is now a &lt;em&gt;decision&lt;/em&gt;, not an accident of what laptop you bought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it was actually teaching
&lt;/h2&gt;

&lt;p&gt;The container promise isn't "the same image runs everywhere." It's "the same image runs everywhere &lt;em&gt;that shares the contract it was built against&lt;/em&gt;" — and CPU architecture is part of that contract, an invisible part that used to be uniform and quietly stopped being uniform the day ARM laptops got good.&lt;/p&gt;

&lt;p&gt;That's the pattern behind almost every "works on my machine" that survives containerization: some assumption from your environment rode along inside the image without you noticing — an architecture, a mounted file that only exists locally, an env var your shell sets and prod doesn't. The container didn't lie. It faithfully packaged your assumptions and carried them somewhere the assumptions weren't true.&lt;/p&gt;

&lt;p&gt;The fix is always the same discipline: make the invisible contract explicit. Build for the target, not the desk you're sitting at.&lt;/p&gt;




&lt;p&gt;I keep the full library of Docker gotchas like this one — the diagnostic commands, the root cause, the prevention — for the next time one of them eats an afternoon:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://devopsaitoolkit.com/stacks/docker/" rel="noopener noreferrer"&gt;Docker troubleshooting toolkit&lt;/a&gt;, and the &lt;a href="https://devopsaitoolkit.com/blog/docker-error-executable-file-not-found-in-path/" rel="noopener noreferrer"&gt;&lt;code&gt;executable file not found in $PATH&lt;/code&gt; guide&lt;/a&gt; for its close cousin (the &lt;em&gt;other&lt;/em&gt; "your binary won't run" error).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's your favorite "same image, different result" story? The ARM-laptop-to-x86-cluster one has bitten a lot of people since about 2021 — I doubt I'm the last.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>debugging</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The 10 Docker Errors That Waste the Most Time (and the One-Line Fix)</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Fri, 10 Jul 2026 01:54:11 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/the-10-docker-errors-that-waste-the-most-time-and-the-one-line-fix-45mg</link>
      <guid>https://dev.to/devopsaitoolkit/the-10-docker-errors-that-waste-the-most-time-and-the-one-line-fix-45mg</guid>
      <description>&lt;p&gt;Docker is fantastic right up until it throws one of its greasy, context-free error messages at you and you lose twenty minutes to a thing that has a one-line fix. I've been collecting these — the exact strings, and the first thing to check for each. Here are the ten that eat the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;Cannot connect to the Docker daemon at unix:///var/run/docker.sock&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The engine isn't reachable. In order of likelihood: the daemon isn't running (&lt;code&gt;systemctl status docker&lt;/code&gt;), you're not in the &lt;code&gt;docker&lt;/code&gt; group (&lt;code&gt;sudo usermod -aG docker $USER&lt;/code&gt;, then log out and back in), or you're pointing at the wrong &lt;code&gt;DOCKER_HOST&lt;/code&gt;. It's almost never Docker being broken — it's Docker not being &lt;em&gt;up&lt;/em&gt; or you not being &lt;em&gt;allowed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-cannot-connect-to-docker-daemon/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;no space left on device&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Docker hoards. Dangling images, stopped containers, unused volumes and build cache pile up on the Docker root disk. &lt;code&gt;docker system df&lt;/code&gt; shows you where it went; &lt;code&gt;docker system prune -a --volumes&lt;/code&gt; reclaims it (read what it'll delete first). If &lt;code&gt;df -h&lt;/code&gt; says you have space but Docker disagrees, you may be out of inodes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-no-space-left-on-device/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;Bind for 0.0.0.0:8080 failed: port is already allocated&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Something already owns that port — often a container you forgot was running. &lt;code&gt;docker ps&lt;/code&gt; to find it, or &lt;code&gt;ss -tlnp | grep 8080&lt;/code&gt; for a non-Docker process. Stop the holder or map to a different host port.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-port-is-already-allocated/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;pull access denied ... repository does not exist or may require 'docker login'&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Three flavors: the image name/tag is wrong, it's a private registry and you're not authenticated (&lt;code&gt;docker login&lt;/code&gt;), or you've hit Docker Hub's anonymous pull rate limit. The error says "does not exist OR requires login" for a reason — check both.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-pull-access-denied/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;exec format error&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You built the image for one CPU architecture and ran it on another — the classic Apple Silicon (&lt;code&gt;arm64&lt;/code&gt;) build landing on an &lt;code&gt;amd64&lt;/code&gt; server. Build multi-arch with &lt;code&gt;docker buildx&lt;/code&gt;, or pin &lt;code&gt;--platform&lt;/code&gt; to match your target.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. &lt;code&gt;OCI runtime create failed&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A low-level container-start failure. The useful part is always &lt;em&gt;after&lt;/em&gt; the colon — a missing binary, a bad mount, a permissions problem. Read the full message; &lt;code&gt;OCI runtime create failed&lt;/code&gt; itself tells you nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-oci-runtime-create-failed/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;executable file not found in $PATH&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Your &lt;code&gt;CMD&lt;/code&gt; or &lt;code&gt;ENTRYPOINT&lt;/code&gt; points at a binary the image doesn't have — often because a slim/distroless base doesn't ship a shell, or you assumed a tool was installed. Check exec-form vs shell-form and confirm the binary actually exists in the final layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-executable-file-not-found-in-path/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;TLS handshake timeout&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Usually not a cert problem — it's a network path issue (a proxy, MTU, or firewall) between you and the registry, masquerading as TLS. Test raw connectivity before you touch certificates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopsaitoolkit.com/blog/docker-error-tls-handshake-timeout/" rel="noopener noreferrer"&gt;Full guide →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. &lt;code&gt;failed to compute cache key: ... not found&lt;/code&gt; (COPY/ADD)
&lt;/h2&gt;

&lt;p&gt;Your Dockerfile is trying to &lt;code&gt;COPY&lt;/code&gt; a file that isn't in the build context — either the path is wrong, or &lt;code&gt;.dockerignore&lt;/code&gt; is excluding it. Remember paths are relative to the context root, not the Dockerfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. &lt;code&gt;Conflict. The container name "/x" is already in use&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A container with that name already exists (running or stopped). &lt;code&gt;docker rm x&lt;/code&gt; to remove the old one, or use &lt;code&gt;--rm&lt;/code&gt; / a fresh name. Common in CI where a previous run didn't clean up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Nearly every Docker error puts the &lt;em&gt;useful information after the colon&lt;/em&gt; and a generic category before it. &lt;code&gt;OCI runtime create failed&lt;/code&gt; is the category; the cause is the clause you skimmed past. Train yourself to read to the end of the line before you start googling.&lt;/p&gt;

&lt;p&gt;I keep complete guides for all of these — and about eighty more Docker errors — each with the diagnostic workflow, a worked root-cause example, and the prevention checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://devopsaitoolkit.com/stacks/docker/" rel="noopener noreferrer"&gt;Docker troubleshooting toolkit&lt;/a&gt; — the top errors, launcher, and runbooks in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which Docker error has personally cost you the most hours? Genuinely curious which of these tops the list for other people.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I Cut a Docker Image From 1.2GB to 180MB</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Thu, 09 Jul 2026 02:32:56 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/how-i-cut-a-docker-image-from-12gb-to-180mb-2ah8</link>
      <guid>https://dev.to/devopsaitoolkit/how-i-cut-a-docker-image-from-12gb-to-180mb-2ah8</guid>
      <description>&lt;p&gt;A while back I inherited a service whose Docker image was 1.2GB. Pulls were slow, the CI cache was useless, and the deploy step took long enough that people context-switched away and forgot about it. I got it down to about 180MB without changing a line of application code. Here's exactly what moved the needle, roughly in order of impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Multi-stage builds (the big one)
&lt;/h2&gt;

&lt;p&gt;The single biggest win. The original Dockerfile built the app and shipped the &lt;em&gt;entire build toolchain&lt;/em&gt; along with it — compilers, dev headers, the full package cache. None of that is needed at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# build stage — has all the heavy tooling&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# runtime stage — starts clean, copies only the artifact&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime image never contains the build tools. That alone took roughly 500MB off.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pick a smaller base
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;node:20&lt;/code&gt; is Debian with everything. &lt;code&gt;node:20-slim&lt;/code&gt; drops a couple hundred MB. If your app is a static binary (Go, Rust) you can go all the way to &lt;code&gt;distroless&lt;/code&gt; or &lt;code&gt;scratch&lt;/code&gt; and ship &lt;em&gt;just the binary&lt;/em&gt; — no shell, no package manager, no OS to speak of. Smaller base = smaller image and a smaller attack surface, which your security team will also thank you for.&lt;/p&gt;

&lt;p&gt;The trade-off: distroless has no shell, so &lt;code&gt;docker exec ... sh&lt;/code&gt; won't work for debugging. Know that going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Order layers by how often they change
&lt;/h2&gt;

&lt;p&gt;Docker caches layers top-down and invalidates everything after the first change. If you &lt;code&gt;COPY . .&lt;/code&gt; before installing dependencies, &lt;em&gt;every code change busts your dependency cache&lt;/em&gt; and reinstalls everything.&lt;/p&gt;

&lt;p&gt;Copy your lockfile and install deps &lt;strong&gt;first&lt;/strong&gt;, then copy the rest of the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci          &lt;span class="c"&gt;# cached until dependencies actually change&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .            # changes every commit, but deps stay cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This didn't shrink the final image much, but it turned a 4-minute rebuild into a 20-second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Add a real &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Without it, &lt;code&gt;COPY . .&lt;/code&gt; drags your entire &lt;code&gt;.git&lt;/code&gt; history, &lt;code&gt;node_modules&lt;/code&gt;, local &lt;code&gt;.env&lt;/code&gt; files, test fixtures, and CI logs into the build context — bloating the image &lt;em&gt;and&lt;/em&gt; leaking things you don't want baked into a layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;git&lt;/span&gt;
&lt;span class="n"&gt;node_modules&lt;/span&gt;
*.&lt;span class="n"&gt;log&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;*
&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="n"&gt;coverage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Collapse and clean up &lt;code&gt;RUN&lt;/code&gt; layers
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;RUN&lt;/code&gt; is a layer, and deleting files in a &lt;em&gt;later&lt;/em&gt; layer doesn't shrink the earlier one. Install, use, and clean up in a single &lt;code&gt;RUN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; some-tool &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rm&lt;/code&gt; has to be in the same &lt;code&gt;RUN&lt;/code&gt; as the &lt;code&gt;apt-get&lt;/code&gt;, or the cache still ships in the layer beneath it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image size&lt;/td&gt;
&lt;td&gt;1.2 GB&lt;/td&gt;
&lt;td&gt;~180 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold pull&lt;/td&gt;
&lt;td&gt;~90s&lt;/td&gt;
&lt;td&gt;~12s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached rebuild&lt;/td&gt;
&lt;td&gt;~4 min&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this is exotic — it's multi-stage, a slimmer base, layer order, &lt;code&gt;.dockerignore&lt;/code&gt;, and cleaning up in place. But together they turn a deploy you dread into one you don't think about.&lt;/p&gt;

&lt;p&gt;If you want the deeper reference — including the Docker errors these optimizations sometimes surface (&lt;code&gt;no space left on device&lt;/code&gt;, cache-key failures, and friends) — I keep a full set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://devopsaitoolkit.com/stacks/docker/" rel="noopener noreferrer"&gt;Docker troubleshooting toolkit&lt;/a&gt; and the &lt;a href="https://devopsaitoolkit.com/blog/docker-error-no-space-left-on-device/" rel="noopener noreferrer"&gt;&lt;code&gt;no space left on device&lt;/code&gt; guide&lt;/a&gt; for when the build disk fills up mid-optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's the smallest you've gotten a &lt;em&gt;real&lt;/em&gt; production image (not a hello-world)? Always looking for tricks I haven't tried.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>performance</category>
    </item>
    <item>
      <title>7 Dockerfile Mistakes That Are Quietly Costing You</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:55:42 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/7-dockerfile-mistakes-that-are-quietly-costing-you-5a5n</link>
      <guid>https://dev.to/devopsaitoolkit/7-dockerfile-mistakes-that-are-quietly-costing-you-5a5n</guid>
      <description>&lt;p&gt;Most Dockerfiles work. That's the problem — "it builds and runs" hides a lot of quiet costs in security, speed, and size that don't announce themselves until an audit, an incident, or a cloud bill does it for them. Here are seven mistakes I see constantly, and what to do instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Running as root
&lt;/h2&gt;

&lt;p&gt;By default, the process in your container runs as root — and if someone breaks out, they're root on a surface they shouldn't be. Add a non-root user and switch to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; 10001 appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap, and it closes off a whole category of "well, at least it wasn't root" incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;FROM some-image:latest&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;latest&lt;/code&gt; is not a version — it's "whatever was newest when this happened to build." Two builds a week apart can produce different images with no diff to explain it, and a surprise base upgrade is a fun way to spend a Friday. Pin a specific tag, ideally by digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20.11.1-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Baking secrets into layers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;COPY .env .&lt;/code&gt; or &lt;code&gt;ARG API_KEY&lt;/code&gt; followed by using it — and now the secret lives in an image layer &lt;em&gt;forever&lt;/em&gt;, recoverable by anyone who pulls the image, even if a later layer deletes the file. Layers are immutable and additive; you can't delete your way out of a leak. Use build secrets (&lt;code&gt;--mount=type=secret&lt;/code&gt;) or inject at runtime, never at build.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Without one, &lt;code&gt;COPY . .&lt;/code&gt; sweeps your &lt;code&gt;.git&lt;/code&gt; directory, local env files, &lt;code&gt;node_modules&lt;/code&gt;, and test data into the build context — bloating the image and, worse, potentially baking credentials and history into a layer. A five-line &lt;code&gt;.dockerignore&lt;/code&gt; is one of the highest-leverage files in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Layer order that destroys your cache
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci      &lt;span class="c"&gt;# ← reinstalls on EVERY code change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker invalidates every layer after the first change. Copy the lockfile and install dependencies &lt;em&gt;before&lt;/em&gt; copying the rest of your source, so a one-line code change doesn't trigger a full reinstall. This is a build-speed bug hiding as a style choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Leaving package manager cruft in the image
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That leaves the apt lists sitting in the layer. And cleaning them in a &lt;em&gt;separate&lt;/em&gt; &lt;code&gt;RUN&lt;/code&gt; doesn't help — the bytes are already committed to the earlier layer. Do it all in one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. No &lt;code&gt;HEALTHCHECK&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Without one, Docker (and your orchestrator) only knows whether the &lt;em&gt;process&lt;/em&gt; is alive — not whether the app can actually serve. A container can be "up" and completely wedged. A healthcheck that hits a real endpoint lets the platform notice and recycle it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=3s \&lt;/span&gt;
  CMD curl -f http://localhost:8080/healthz || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Make sure that endpoint checks something real, not just "is the web server running" — but that's a whole other article.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The theme
&lt;/h2&gt;

&lt;p&gt;None of these break the build. They surface later — as a security finding, a slow pipeline, a bloated registry, or a container that's "healthy" while failing. The fixes are all a line or two; the hard part is remembering them at 4 p.m. on a Dockerfile you just want to ship.&lt;/p&gt;

&lt;p&gt;Two things that help me not forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I run Dockerfiles and Compose files through a &lt;a href="https://devopsaitoolkit.com/validators/" rel="noopener noreferrer"&gt;client-side validator&lt;/a&gt; (runs in your browser, nothing uploaded) to catch the structural stuff before it ships.&lt;/li&gt;
&lt;li&gt;And I keep a &lt;a href="https://devopsaitoolkit.com/stacks/docker/" rel="noopener noreferrer"&gt;Docker troubleshooting toolkit&lt;/a&gt; for when one of these mistakes graduates into an actual error at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which of these did you learn the hard way? Mine was #3, and I think about it more than I'd like to admit.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Reading Loki Logs With AI: Patterns That Work</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:43:17 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/reading-loki-logs-with-ai-patterns-that-work-255m</link>
      <guid>https://dev.to/devopsaitoolkit/reading-loki-logs-with-ai-patterns-that-work-255m</guid>
      <description>&lt;p&gt;If you've adopted Loki for log aggregation, you've probably had this moment: you need to find something in your logs &lt;em&gt;right now&lt;/em&gt;, you open Grafana, and you stare at the empty LogQL query bar trying to remember whether it's &lt;code&gt;|=&lt;/code&gt; or &lt;code&gt;=~&lt;/code&gt; for the substring filter. Five minutes later you've cobbled something together, run it, gotten zero results, and you're not sure if the query is wrong or the logs aren't there.&lt;/p&gt;

&lt;p&gt;This is the kind of friction AI is good at removing. LogQL has a small, structured grammar; the model knows it; you describe what you want; you get a working query. But — and this is the recurring theme — the model will also sometimes produce queries that are syntactically valid and semantically wrong, and there's a specific way to catch that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basics AI handles well
&lt;/h2&gt;

&lt;p&gt;These are the patterns I use AI for daily without much verification:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Label filter + substring&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give me a LogQL query that finds error messages from the &lt;code&gt;payments&lt;/code&gt; app in the &lt;code&gt;production&lt;/code&gt; namespace over the last hour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reliable output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{namespace="production", app="payments"} |= "error"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Counting by level&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Count the rate of log entries per level for the &lt;code&gt;web&lt;/code&gt; app over 5-minute windows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reliable output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by (level)(rate({app="web"} | json | __error__="" [5m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JSON parsing + extraction&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Show me request durations from the &lt;code&gt;api&lt;/code&gt; app where the JSON &lt;code&gt;duration_ms&lt;/code&gt; field is greater than 1000.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reliable output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="api"} | json | duration_ms &amp;gt; 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are exactly the kinds of queries that take me 5 minutes to write from memory and 5 seconds to get from Claude. Worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI gets LogQL wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Confusing PromQL and LogQL syntax
&lt;/h3&gt;

&lt;p&gt;The model has read more PromQL than LogQL, and sometimes it leaks. You'll get a query like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate({app="web"}[5m]) by (level)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;by (level)&lt;/code&gt; placement is PromQL syntax. In LogQL, you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by (level)(rate({app="web"}[5m]))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Grafana editor catches this and tells you it's a syntax error. But if you're using &lt;code&gt;logcli&lt;/code&gt; or the API directly, you might get a confusing error and not realize the issue is structural.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using labels that aren't indexed
&lt;/h3&gt;

&lt;p&gt;LogQL is fast when you filter on indexed labels (the ones in &lt;code&gt;{}&lt;/code&gt;). It's slow when you filter on extracted fields after &lt;code&gt;| json&lt;/code&gt;. The model doesn't know which of your labels are indexed; it'll happily put high-cardinality fields in the curly braces.&lt;/p&gt;

&lt;p&gt;If you ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find all requests from user 12345.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You might get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="api", user_id="12345"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;user_id&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; a stream label (and it usually shouldn't be — it's high cardinality), this query is invalid and Loki rejects it. The correct query is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="api"} | json | user_id="12345"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you describe the query, tell the model which labels are in the stream selector vs which are JSON fields. Otherwise it guesses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inventing operators
&lt;/h3&gt;

&lt;p&gt;LogQL has &lt;code&gt;|=&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;|~&lt;/code&gt;, &lt;code&gt;!~&lt;/code&gt; for line filtering. The model sometimes invents &lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;like&lt;/code&gt;, or other operators that don't exist. The query fails with a parse error.&lt;/p&gt;

&lt;p&gt;Easy to catch — the parse error tells you exactly which token is wrong — but worth knowing as a class of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Confused unwrap behavior
&lt;/h3&gt;

&lt;p&gt;For metric queries over log values (rate of a histogram, sum of a counter), you need &lt;code&gt;| unwrap&lt;/code&gt;. The model sometimes uses &lt;code&gt;| unwrap&lt;/code&gt; incorrectly or skips it when needed. The query runs but returns 0 or NaN, which looks like "no data" but is really "wrong aggregation."&lt;/p&gt;

&lt;p&gt;This one is harder to catch because the query &lt;em&gt;executes&lt;/em&gt;. You have to read the result and notice it's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow for unfamiliar log shapes
&lt;/h2&gt;

&lt;p&gt;When you're investigating logs you don't normally look at — different team's service, vendor product, etc. — there's a specific sequence I use:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Get a sample
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="unfamiliar-service"} | json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this against a small time window. Grafana shows you the parsed fields. Now you know what's available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Show the AI the sample
&lt;/h3&gt;

&lt;p&gt;Paste a couple of representative log lines (sanitized) into Claude with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here are 3 sample log lines from a service I don't usually monitor. They're JSON. Tell me what each field appears to mean and what would be good labels to filter on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model reads the structure and tells you which fields are useful. This takes 30 seconds and gives you a mental model of the log shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Generate the query
&lt;/h3&gt;

&lt;p&gt;Now you can ask for a specific query with confidence that the field names you give the model are real:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate a LogQL query that filters the &lt;code&gt;unfamiliar-service&lt;/code&gt; app for entries where &lt;code&gt;status_code&lt;/code&gt; is 500 or 503 and &lt;code&gt;duration_ms&lt;/code&gt; is over 200.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result will use the correct field names because you told the model what they are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Verify before alerting
&lt;/h3&gt;

&lt;p&gt;If the query is going into an alert, run it against historical data and check the results. The model doesn't know your baseline. A query that returns "no data" right now might return huge volumes during a normal incident, or vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  A trap worth flagging
&lt;/h2&gt;

&lt;p&gt;Loki's query frontend caches results aggressively. If you're iterating on a query and the AI changed something subtle, you might get cached results from the previous query and think your change didn't take effect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; When iterating, change the time range slightly between queries (or use &lt;code&gt;instant&lt;/code&gt; queries). This bypasses the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs as a debugging input for AI
&lt;/h2&gt;

&lt;p&gt;A separate but related use case: pasting logs &lt;em&gt;into&lt;/em&gt; the AI to get help debugging. This works better than I expected for logs that have clear structure (JSON, logfmt) and worse for noisy unstructured logs.&lt;/p&gt;

&lt;p&gt;The trick is to paste a window around the suspected issue — not the whole log file. Five minutes around the incident is usually plenty. The model spots patterns ("you have 47 OOM kills in this window, all on pods in the &lt;code&gt;payments&lt;/code&gt; namespace") that I'd miss manually.&lt;/p&gt;

&lt;p&gt;But: keep the volume sane. A 5MB log paste degrades the model's attention. If you have 10,000 lines, filter to the relevant subset first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that ties it together
&lt;/h2&gt;

&lt;p&gt;Most of what I've described is the same shape: give the model a small amount of accurate context (sample logs, label names, time range), then let it generate the LogQL. The failures all come from skipping the context step and hoping the model can guess your schema.&lt;/p&gt;

&lt;p&gt;For prompts on Loki specifically, see the &lt;a href="https://dev.to/prompts/loki-log-aggregation-design/"&gt;Loki log aggregation design&lt;/a&gt; and the &lt;a href="https://dev.to/prompts/grafana-logs-panel-derived-fields/"&gt;Grafana logs panel patterns&lt;/a&gt;. For PromQL, the related &lt;a href="https://dev.to/prompts/promql-query-optimization/"&gt;PromQL query optimization&lt;/a&gt; prompt covers similar territory.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/reading-loki-logs-with-ai/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>loki</category>
      <category>logs</category>
      <category>logql</category>
    </item>
    <item>
      <title>The Pod That Lied: A Kubernetes Readiness Probe War Story</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Tue, 07 Jul 2026 07:33:12 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/the-pod-that-lied-a-kubernetes-readiness-probe-war-story-4kfg</link>
      <guid>https://dev.to/devopsaitoolkit/the-pod-that-lied-a-kubernetes-readiness-probe-war-story-4kfg</guid>
      <description>&lt;p&gt;Last time I told you to bring coffee. I did. This one didn't start at 2 a.m. — it started at 10:40 on a perfectly ordinary Tuesday, which, if anything, is worse. There's a particular kind of dread that only arrives in full daylight, when you're well-rested and caffeinated and therefore have no excuse for not understanding what's happening. And I did not understand what was happening.&lt;/p&gt;

&lt;p&gt;Welcome back to &lt;strong&gt;Troubleshooting Kubernetes&lt;/strong&gt;. Today's episode is about the most unsettling category of outage there is: the one where nothing is broken. Every light is green. Every pod is &lt;code&gt;Running&lt;/code&gt;. Every dashboard is calm. And your users are getting 500s.&lt;/p&gt;

&lt;p&gt;I've been doing this long enough — a couple of decades and change, across a few places I've been lucky to work — to have developed a healthy distrust of green. Green is not a fact. Green is a &lt;em&gt;claim&lt;/em&gt;. And on that Tuesday, something was lying to me with a very straight face.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom that wasn't there
&lt;/h2&gt;

&lt;p&gt;The error rate on the checkout API had crept from basically-zero to about 18% over twenty minutes. Not a spike. A &lt;em&gt;creep&lt;/em&gt; — the kind that makes you wonder if it's real or if the graph is just having feelings. So, first move, always: is this real, or is the monitoring lying?&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;# hit it through the front door, a bunch of times&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 20&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://checkout.internal/api/health-of-a-real-endpoint
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real. About one in five came back &lt;code&gt;500&lt;/code&gt;. And "about one in five" is a &lt;em&gt;clue&lt;/em&gt;, not just a number — an intermittent failure spread across requests usually means it's spread across &lt;em&gt;replicas.&lt;/em&gt; Some of the pods behind the Service were fine. Some were not. The load balancer was cheerfully dealing everyone a random card from a deck that had a few jokers in it.&lt;/p&gt;

&lt;p&gt;So I went to look at the pods, fully expecting to find a couple of them crash-looping or &lt;code&gt;NotReady&lt;/code&gt; or otherwise waving their hands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; checkout &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five replicas. &lt;code&gt;Running&lt;/code&gt;. &lt;code&gt;1/1&lt;/code&gt;. &lt;code&gt;Ready&lt;/code&gt;. Every last one of them, calm as a millpond. Zero restarts. According to Kubernetes, this service was in perfect health. According to the customers, it was on fire. Those two things cannot both be true, and yet there they were, both being true, mocking me over my second coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the jokers
&lt;/h2&gt;

&lt;p&gt;If the Service is routing to five pods and some are bad, I want to know &lt;em&gt;which&lt;/em&gt; ones — go around the load balancer and interrogate each pod directly. The endpoints list tells you who's actually in rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get endpoints checkout-api &lt;span class="nt"&gt;-n&lt;/span&gt; checkout &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five pod IPs, present and accounted for, all listed as ready targets. Fine. Let's talk to them one at a time.&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;# talk to a single pod, bypassing the Service&lt;/span&gt;
kubectl port-forward pod/checkout-api-6c8b9-x2k7p 8080:8080 &lt;span class="nt"&gt;-n&lt;/span&gt; checkout
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/api/checkout/quote &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{...}'&lt;/span&gt; &lt;span class="c"&gt;# returns 500&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/healthz                        &lt;span class="c"&gt;# returns 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. On the bad replicas, the &lt;em&gt;real&lt;/em&gt; endpoint threw a 500, and the &lt;em&gt;health&lt;/em&gt; endpoint returned a serene, confident &lt;code&gt;200 OK&lt;/code&gt;. The pod was, in the most literal sense, telling Kubernetes it was ready to serve — while being completely unable to serve.&lt;/p&gt;

&lt;p&gt;Two of the five were doing this. And Kubernetes, being a faithful and literal machine, kept both of them in the endpoints list and kept dealing them to real customers, because they had passed the only test it knew to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lie, and who told it
&lt;/h2&gt;

&lt;p&gt;Here's the readiness probe those pods were configured with — and I'd bet real money it looks familiar, because it's the one everybody writes on their first day and never revisits:&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;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here, roughly, is what &lt;code&gt;/healthz&lt;/code&gt; did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /healthz  →  "am I an HTTP server that is currently running?"  →  200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole check. It confirms the process is up and the socket is listening. It does not ask the one question that actually matters to a customer: &lt;em&gt;can you do your job right now?&lt;/em&gt; It never touches the database. It never checks the connection pool. It is a smoke detector wired to confirm that it has electricity.&lt;/p&gt;

&lt;p&gt;What had actually happened: the database had done a failover about twenty-five minutes earlier — routine, expected, mostly graceful. Most of the app replicas noticed, dropped their stale connections, and reconnected to the new primary. Two of them didn't. Their connection pools were wedged full of dead connections to an IP that no longer answered, so every real query timed out into a 500. But the HTTP server? Still up. Still listening. Still cheerfully answering &lt;code&gt;/healthz&lt;/code&gt; with a &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the pods weren't broken from Kubernetes' point of view. They were doing exactly what they'd promised. The lie wasn't Kubernetes'. Kubernetes was the most honest actor in the whole incident — it did precisely what it was told, with no imagination whatsoever. The lie was in the probe. We had told it that "the web server is up" meant "this pod can serve customers," and it had believed us, because why wouldn't it. The green dashboard was technically, uselessly accurate.&lt;/p&gt;

&lt;p&gt;I have a lot of respect for that, honestly. It's the same trait that saved my data in the last installment — Kubernetes doing the literal thing, refusing to be clever. It'll protect you from yourself and it'll faithfully execute your mistakes with equal diligence. The machine isn't the problem. The machine is a mirror.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the jokers out of the deck
&lt;/h2&gt;

&lt;p&gt;Two things to do, in order: stop the bleeding, then fix the actual bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop the bleeding.&lt;/strong&gt; The two wedged replicas just needed their connection pools reset, and the fastest way to reset a pod's everything is to let the Deployment give you a fresh one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod checkout-api-6c8b9-x2k7p checkout-api-6c8b9-9m4tz &lt;span class="nt"&gt;-n&lt;/span&gt; checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New pods came up, connected cleanly to the new primary, and the 500s stopped inside a minute. Error rate back to zero. Crisis over. And this is exactly the moment where a tired engineer declares victory, closes the incident, and goes to lunch having fixed &lt;em&gt;nothing&lt;/em&gt; — because the bug wasn't the wedged pool. Connection pools wedge. Databases fail over. That's Tuesday. The bug was that &lt;strong&gt;a pod that couldn't serve was allowed to keep serving.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the actual bug.&lt;/strong&gt; The readiness probe has to check what actually matters. A real &lt;code&gt;/readyz&lt;/code&gt; that returns &lt;code&gt;503&lt;/code&gt; when the pool can't hand out a working connection:&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;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/readyz&lt;/span&gt;          &lt;span class="c1"&gt;# checks a DB connection, not just the socket&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a wedged replica reports itself &lt;em&gt;not ready&lt;/em&gt;, Kubernetes pulls it from the Service endpoints automatically, and traffic routes only to pods that can actually do the work. The self-healing you thought you already had, you now actually have.&lt;/p&gt;

&lt;p&gt;But — and this is the part people skip, the part that turns a fix into a second outage — &lt;strong&gt;do not get greedy with your readiness probe.&lt;/strong&gt; If every replica shares one database, and you wire readiness directly to that database, then the &lt;em&gt;next&lt;/em&gt; time the DB hiccups for fifteen seconds, all five pods simultaneously report not-ready, Kubernetes yanks &lt;em&gt;every&lt;/em&gt; endpoint, and now you've converted a fifteen-second blip into a total, self-inflicted outage with a thundering-herd reconnect on the far side. The probe should reflect &lt;em&gt;this pod's&lt;/em&gt; ability to serve, with enough tolerance (&lt;code&gt;failureThreshold&lt;/code&gt;, sane timing) that a shared, transient dependency wobble doesn't get amplified into a coordinated group suicide. Readiness pulls a &lt;em&gt;sick&lt;/em&gt; pod from rotation. It should not pull a &lt;em&gt;nervous&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;And keep liveness and readiness doing different jobs. Liveness answers "should I be &lt;em&gt;restarted&lt;/em&gt;?" — reserve it for genuinely wedged, unrecoverable states, because a liveness probe tied to a shared dependency is how you turn a database blip into a cluster-wide restart storm. Readiness answers "should I get &lt;em&gt;traffic&lt;/em&gt;?" Conflate them and you'll eventually get both failure modes at once, which is a bad day I'll tell you about some other time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why green lies, and why I still love this
&lt;/h2&gt;

&lt;p&gt;The lesson underneath the lesson: a health check is a &lt;em&gt;promise your application makes about itself&lt;/em&gt;, and your monitoring is only ever as honest as that promise. The dashboard wasn't wrong. It was faithfully reporting a claim that happened to be worthless. If you take one thing from this: &lt;strong&gt;write the probe that checks the thing your users actually depend on, not the thing that's easy to check.&lt;/strong&gt; The gap between those two is where 10:40-on-a-Tuesday lives.&lt;/p&gt;

&lt;p&gt;That gap — the difference between "the process is up" and "the thing actually works" — is basically the whole reason I started keeping structured notes on this stuff, and eventually turned them into a site. When you're staring at a green dashboard and a red reality, the useful thing isn't a metrics graph, it's a &lt;em&gt;hypothesis&lt;/em&gt;: "some replicas can't reach a dependency; check the endpoints and probe each pod directly." I'll sometimes paste the raw logs into the &lt;a href="https://devopsaitoolkit.com/dashboard/incident-response/" rel="noopener noreferrer"&gt;AI incident assistant&lt;/a&gt; I keep on the site just to get to that first hypothesis faster — not because it knows the answer, but because at minute three of an incident, a decent starting question is worth more than another dashboard. Sanitize your secrets before you paste, obviously. The judgment is still yours; the tool just hands you the thread to pull.&lt;/p&gt;

&lt;p&gt;Here's the thing I keep coming back to, twenty-odd years in: this incident was &lt;em&gt;beautiful&lt;/em&gt;, in the specific way that only a good bug is beautiful. Nothing was broken. Every component behaved exactly as designed. And the emergent result was still wrong, because of a single lazy assumption baked into six lines of YAML that someone — probably me, honestly — wrote in a hurry two years ago and never looked at again. Untangling that, watching the green dashboard finally &lt;em&gt;mean&lt;/em&gt; something, is a feeling I have never once gotten tired of. The pages are annoying. The 10:40 Tuesdays are worse. And I would not trade this job for a quieter one, because a quieter one wouldn't let me do &lt;em&gt;that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next time in &lt;strong&gt;Troubleshooting Kubernetes&lt;/strong&gt;: the DNS lookup that resolved perfectly from my laptop, resolved perfectly from the node, and failed only — &lt;em&gt;only&lt;/em&gt; — from inside the pod. Bring coffee. Bring patience. Bring a copy of the ndots documentation you've never actually read.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The running set of Kubernetes runbooks, probe patterns, and error guides I keep so I'm not re-deriving them mid-incident lives on the &lt;a href="https://devopsaitoolkit.com/stacks/kubernetes/" rel="noopener noreferrer"&gt;Kubernetes toolkit&lt;/a&gt;. Green is a claim. Make your probes tell the truth.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/the-pod-that-lied-kubernetes-readiness-probe-war-story/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>readinessprobes</category>
      <category>reliability</category>
    </item>
    <item>
      <title>The Volume That Wouldn't Let Go: A Kubernetes PVC War Story</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Tue, 07 Jul 2026 07:33:08 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/the-volume-that-wouldnt-let-go-a-kubernetes-pvc-war-story-3b9b</link>
      <guid>https://dev.to/devopsaitoolkit/the-volume-that-wouldnt-let-go-a-kubernetes-pvc-war-story-3b9b</guid>
      <description>&lt;p&gt;The page came in at 2:14 a.m., which is the only time pages ever come in. I have a theory that PagerDuty holds them in a little queue until it's certain you're in the deepest part of your sleep cycle, and then it releases them all at once, like a cat knocking a glass off a table while making eye contact. I've been doing this work for twenty-five years — long enough to have carried a literal pager at Yahoo, long enough to have learned resilience the hard way at Netflix, long enough that a 2 a.m. alert no longer produces adrenaline so much as a kind of tired affection. Here we go again. Let's see what you've got.&lt;/p&gt;

&lt;p&gt;What it had was a payments-adjacent service that had stopped serving. Not slow. Not flapping. Stopped. The kind of outage where the graph doesn't decline, it just falls off the edge of the world.&lt;/p&gt;

&lt;p&gt;This is the first in a series I'm calling &lt;strong&gt;Troubleshooting Kubernetes&lt;/strong&gt; — war stories from the cluster, with the actual fixes, told by someone who genuinely loves this work and is also deeply, professionally tired. Those two things aren't in tension. They never have been.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom, and the small lie it told
&lt;/h2&gt;

&lt;p&gt;First move, always the cheapest one: look at the pods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; payments &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of three replicas were &lt;code&gt;Running&lt;/code&gt;. The third — the one that had presumably rescheduled after something went sideways — was sitting in &lt;code&gt;ContainerCreating&lt;/code&gt;, and had been for six minutes. Six minutes is an interesting number in Kubernetes. Hold onto it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ContainerCreating&lt;/code&gt; is Kubernetes' way of telling you it's &lt;em&gt;trying&lt;/em&gt;, which is the most maddening status there is. It's not an error. It's not a crash loop you can grep for. It's a shrug. So you ask it to be specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod payments-api-7d9f5-abcde &lt;span class="nt"&gt;-n&lt;/span&gt; payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there, down in the Events, was the sentence that would define my next twenty minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning  FailedAttachVolume   attachdetach-controller
  Multi-Attach error for volume "pvc-4a1b...": Volume is already
  exclusively attached to one node and can't be attached to another
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you've never met this error, it reads like an accusation. &lt;em&gt;Multi-Attach.&lt;/em&gt; It sounds like I did something greedy — like I tried to bolt the same disk onto two machines at once out of hubris. I did not. Kubernetes did, on my behalf, and then caught itself, and now it's blaming me. Classic.&lt;/p&gt;

&lt;p&gt;Here's the honest part: my very first instinct was wrong. My tired brain went, "storage backend's having a bad night, page the storage team, go back to bed." That's the instinct talking, not the evidence. So I did the thing I always tell junior engineers to do and almost didn't do myself at 2 a.m.: I stopped guessing and started proving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that actually matters
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;ReadWriteOnce&lt;/code&gt; PersistentVolume — RWO, the default most people never think about until it bites them — can be attached to exactly &lt;strong&gt;one node at a time&lt;/strong&gt;. Not one pod. One &lt;em&gt;node&lt;/em&gt;. That distinction is the whole story. The volume was fine. The data was fine. The problem was that the volume was still, according to Kubernetes' bookkeeping, attached to a node the new pod wasn't running on.&lt;/p&gt;

&lt;p&gt;Which node? Let's ask.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get volumeattachment | &lt;span class="nb"&gt;grep &lt;/span&gt;pvc-4a1b
kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;VolumeAttachment&lt;/code&gt; object still pointed at &lt;code&gt;node-17&lt;/code&gt;. And &lt;code&gt;node-17&lt;/code&gt;, per &lt;code&gt;kubectl get nodes&lt;/code&gt;, was sitting there in &lt;code&gt;NotReady&lt;/code&gt;, looking innocent.&lt;/p&gt;

&lt;p&gt;Not gone. Not deleted. &lt;strong&gt;&lt;code&gt;NotReady&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that, right there, is where the error stops being a bug and starts being Kubernetes doing exactly what I'd want it to do, if I weren't so annoyed at it. Because here's the thing the attach-detach controller knows that I, at 2 a.m., had briefly forgotten: a &lt;code&gt;NotReady&lt;/code&gt; node is not a &lt;em&gt;dead&lt;/em&gt; node. It's an &lt;em&gt;unreachable&lt;/em&gt; node. Those are wildly different situations, and the difference is measured in whether or not you corrupt a filesystem.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;node-17&lt;/code&gt; had genuinely died — power gone, instance terminated — then sure, force-detach the volume and let the new pod have it. But if &lt;code&gt;node-17&lt;/code&gt; is merely unreachable — a network partition, a wedged kubelet, a security group someone "cleaned up" — then the old pod might &lt;em&gt;still be alive on that node, still writing to that disk.&lt;/em&gt; Rip the volume away and hand it to a second writer, and you've just mounted the same block device on two hosts at once. On a filesystem that assumes it's the only one home. That's not an outage anymore. That's a data-corruption incident with a much longer postmortem and a much worse tone.&lt;/p&gt;

&lt;p&gt;So Kubernetes waits. By default, roughly six minutes — remember the six minutes? — before the controller-manager will force-detach a volume from an unreachable node. It's not being slow. It's being &lt;em&gt;careful.&lt;/em&gt; It would rather your service be down for six minutes than your data be wrong forever. Given the choice, so would I. Given the choice at 2 a.m., I had to remind myself I agreed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, which was mostly a decision
&lt;/h2&gt;

&lt;p&gt;So the actual work wasn't a command. It was a question: &lt;strong&gt;is &lt;code&gt;node-17&lt;/code&gt; coming back, or is it gone?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I checked the cloud console. The instance was still there, still "running," just not talking to the API server. &lt;code&gt;kubelet&lt;/code&gt; had wedged after a bad network event — the node was up, the workload on it was long dead (the container had exited), but the kubelet hadn't been able to report that fact home. So the volume was &lt;em&gt;safe to move.&lt;/em&gt; No live writer. I just had to convince the control plane of what I'd already confirmed with my own eyes.&lt;/p&gt;

&lt;p&gt;The clean way, when you've verified the node is truly not running your workload, is to let the controller do its job by removing the thing it's protecting:&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;# Only after CONFIRMING nothing is still writing to that volume.&lt;/span&gt;
kubectl delete node node-17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting the &lt;code&gt;Node&lt;/code&gt; object tells the attach-detach controller the truth it couldn't discover on its own: that node is out of the picture. The controller detached the volume, the &lt;code&gt;VolumeAttachment&lt;/code&gt; for the old node disappeared, the pending pod's attach succeeded, the container started, and the graph climbed back out of the hole it had fallen into. Total time down: about nine minutes, most of which was me confirming I wasn't about to do something stupid.&lt;/p&gt;

&lt;p&gt;There's a rougher tool — deleting the &lt;code&gt;VolumeAttachment&lt;/code&gt; object directly to force the issue — and I want to be careful here, because I've seen people reach for it first, like a fire axe, when the situation called for a key. Do that against a node that's still writing and you own the corruption. The order matters: &lt;strong&gt;prove the writer is dead, &lt;em&gt;then&lt;/em&gt; detach.&lt;/strong&gt; Never the other way around. The whole reason the six-minute timer exists is to save you from your own impatience. I have learned to respect it, mostly by having once not respected it.&lt;/p&gt;

&lt;p&gt;If you want the version of this I wish someone had handed me the first time — the exact diagnostic sequence, the "is the node dead or just unreachable" decision tree, the prevention checklist — I ended up writing it down as a proper &lt;a href="https://devopsaitoolkit.com/blog/kubernetes-error-failedattachvolume-multi-attach/" rel="noopener noreferrer"&gt;FailedAttachVolume / Multi-Attach error guide&lt;/a&gt;. Which brings me to the part of this story that isn't about storage at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The epiphany, sitting on my kitchen floor
&lt;/h2&gt;

&lt;p&gt;Here's what I don't usually admit in these. While I was waiting out that timer — because sometimes the correct engineering action is to &lt;em&gt;wait&lt;/em&gt;, and drink water, and not touch anything — I was doing something a little pathetic. I was grepping my own notes. A directory called &lt;code&gt;~/notes/&lt;/code&gt; full of markdown files with names like &lt;code&gt;k8s-storage-stuff.md&lt;/code&gt; and &lt;code&gt;THINGS-I-FORGET.md&lt;/code&gt;, going back years. Some of them referenced Slack threads that no longer exist. One of them referenced a Confluence page that 404s now, at a company I no longer work at, describing a fix I was, at that exact moment, re-deriving from scratch.&lt;/p&gt;

&lt;p&gt;And it landed on me, sitting on the kitchen floor with a laptop and a glass of water at 2:30 in the morning: &lt;em&gt;I had solved this before.&lt;/em&gt; Not this exact incident, but this &lt;em&gt;shape&lt;/em&gt; of problem — the RWO volume stuck on an unreachable node, the six-minute wait, the is-it-dead-or-just-quiet decision. I'd solved it at Netflix. I'd probably solved a cousin of it at Yahoo. I would solve it again. And every single time, the hard part wasn't the fix. &lt;strong&gt;The fix is knowable.&lt;/strong&gt; The hard part was reconstructing the fix, under pressure, from a scattered archaeology of dead links and half-remembered context, while a service was down and a timer was running and my judgment was operating at 40% because it was 2:30 a.m.&lt;/p&gt;

&lt;p&gt;The fix isn't the scarce thing. The &lt;em&gt;fix, made retrievable at the exact moment you need it&lt;/em&gt; — that's the scarce thing.&lt;/p&gt;

&lt;p&gt;That's a solvable problem. That's &lt;em&gt;engineering.&lt;/em&gt; What if the error string you're staring at came pre-attached to the diagnostic sequence, the decision tree, the validator that would've caught the misconfiguration three commits ago, and a runbook written by someone who was calm when they wrote it? What if the next tired person at 2 a.m. didn't have to &lt;em&gt;be me at my best&lt;/em&gt; to get to the answer — they just had to paste the error and get a real starting point?&lt;/p&gt;

&lt;p&gt;I started building it on weekends. It became &lt;a href="https://devopsaitoolkit.com/stacks/kubernetes/" rel="noopener noreferrer"&gt;devopsaitoolkit.com&lt;/a&gt; — a growing pile of the runbooks, error guides, config validators, and copy-paste prompts I'd always wished existed in a single place instead of in my head and a dozen dead wikis. I use AI in it heavily, but not the way the hype cycle wants me to: not as an oracle, just as a fast way to turn a wall of logs into a first hypothesis you then go verify yourself. Sanitize your secrets before you paste anything into a model, always. The judgment stays with you. The tool just gets you to the starting line faster. That's the whole pitch, and it's an honest one, because I built it for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I still love this
&lt;/h2&gt;

&lt;p&gt;I could tell you the tidy lesson — pin your node lifecycle, drain before you terminate, understand your access modes, don't reach for the fire axe — and all of that is true and I'll dig into each of it later in this series. But that's not really why I'm writing this.&lt;/p&gt;

&lt;p&gt;I'm writing this because at 2:47 a.m., after the graph recovered and the pages went quiet, I sat there for a minute in the specific, ridiculous satisfaction of having understood a thing. The system wasn't broken. It was &lt;em&gt;protecting&lt;/em&gt; something — my data, from me — and once I saw it that way, the fix was obvious and even a little beautiful. Kubernetes wasn't fighting me. It was doing its job with more patience than I had.&lt;/p&gt;

&lt;p&gt;That's the part twenty-five years hasn't worn down. The pages are annoying. The org politics are worse. The tooling will disappoint you and the on-call rotation is a tax on your Tuesdays. And underneath all of it, there is still this small, durable joy in taking a thing that made no sense at 2:14 and understanding it completely by 2:47. I get to do that for a living. I'd be lying if I called that anything but lucky.&lt;/p&gt;

&lt;p&gt;Next time in &lt;strong&gt;Troubleshooting Kubernetes&lt;/strong&gt;: the pod that was &lt;code&gt;Running&lt;/code&gt; and completely, confidently wrong. Bring coffee.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Storage access modes are one of those things that are obvious in hindsight and expensive in the moment. The running set of Kubernetes runbooks and error guides I keep — so I'm not rebuilding them under pressure — is exactly what that site turned into. Save the pattern, not just the fix.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/kubernetes-pvc-multi-attach-war-story/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>persistentvolumes</category>
      <category>storage</category>
    </item>
    <item>
      <title>AI Prompt Templates for Prometheus Alerting</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:45:17 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/ai-prompt-templates-for-prometheus-alerting-2a7n</link>
      <guid>https://dev.to/devopsaitoolkit/ai-prompt-templates-for-prometheus-alerting-2a7n</guid>
      <description>&lt;p&gt;Writing good Prometheus alerts is hard. Most alerts are too sensitive (page on every blip), too lax (miss real outages), or missing context (no runbook, no labels, no severity routing). AI assistants are unusually good at the &lt;em&gt;grunt work&lt;/em&gt; of alert authoring — if you prompt them right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why generic alert generators fail
&lt;/h2&gt;

&lt;p&gt;Type "write me a Prometheus alert for high CPU" into any AI and you'll get:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HighCPU&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu_usage &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things wrong already: &lt;code&gt;cpu_usage&lt;/code&gt; isn't a real Prometheus metric, there's no &lt;code&gt;rate()&lt;/code&gt; window, and &lt;code&gt;for: 5m&lt;/code&gt; will flap on every cron job. You need a prompt that anchors the model in production reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The template structure
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://dev.to/prompts/prometheus-alert-rule-generator/"&gt;Prometheus Alert Rule Generator Prompt&lt;/a&gt; enforces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resilient PromQL&lt;/strong&gt; — &lt;code&gt;rate()&lt;/code&gt;, &lt;code&gt;avg_over_time&lt;/code&gt;, or &lt;code&gt;histogram_quantile()&lt;/code&gt; as appropriate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appropriate &lt;code&gt;for:&lt;/code&gt; duration&lt;/strong&gt; — long enough to avoid flap, short enough to detect real outages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity labels and routing&lt;/strong&gt; — &lt;code&gt;severity&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runbook annotation&lt;/strong&gt; — every alert links to a runbook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False-positive analysis&lt;/strong&gt; — the model lists ways the alert could lie.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three patterns worth saving
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1: Rate-based error alerts
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Alert me when the 5-minute error rate exceeds 1% for at least 10 minutes, scoped per service.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generated PromQL pattern:&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;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))&lt;/span&gt;
  &lt;span class="s"&gt;/&lt;/span&gt;
  &lt;span class="s"&gt;sum by (service) (rate(http_requests_total[5m]))&lt;/span&gt;
  &lt;span class="s"&gt;&amp;gt; 0.01&lt;/span&gt;
&lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 2: SLO-based latency
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Alert when p99 latency exceeds my SLO threshold for 10 minutes.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;histogram_quantile(0.99,&lt;/span&gt;
    &lt;span class="s"&gt;sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))&lt;/span&gt;
  &lt;span class="s"&gt;) &amp;gt; 0.8&lt;/span&gt;
&lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 3: Saturation alerts
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Alert when disk on any node will run out in &amp;lt; 4 hours based on current growth rate.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) &amp;lt; 0&lt;/span&gt;
&lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30m&lt;/span&gt;
&lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;predict_linear&lt;/code&gt; pattern is particularly nice — it pages you &lt;em&gt;before&lt;/em&gt; the disk fills, not at 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation: don't trust, verify
&lt;/h2&gt;

&lt;p&gt;Before promoting any AI-generated alert to prod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;promtool check rules my-alerts.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it in your staging Prometheus first. Watch it for 24 hours. Check if it would have fired during recent incidents using &lt;code&gt;promtool test rules&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining alert generation with runbook drafting
&lt;/h2&gt;

&lt;p&gt;A workflow that compounds: ask the same AI to also draft the runbook for the alert it generated. &lt;em&gt;"Now write a runbook for this alert: what should the on-call check first, what are the common causes, and what's the rollback procedure?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You'll have an alert &lt;em&gt;and&lt;/em&gt; a runbook in 5 minutes. Both still need human review — but the blank page is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Companion resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prompts/prometheus-alert-rule-generator/"&gt;Prometheus Alert Rule Generator Prompt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prompts/grafana-dashboard-query-builder/"&gt;Grafana Dashboard Query Builder Prompt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prompts/incident-postmortem-drafter/"&gt;Incident Postmortem Drafter Prompt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/ai-prompt-templates-prometheus-alerting/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>prometheus</category>
      <category>alerting</category>
      <category>promql</category>
    </item>
    <item>
      <title>The Right Way to Pair AI With Terraform Plans</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Fri, 03 Jul 2026 18:42:46 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/the-right-way-to-pair-ai-with-terraform-plans-3i1c</link>
      <guid>https://dev.to/devopsaitoolkit/the-right-way-to-pair-ai-with-terraform-plans-3i1c</guid>
      <description>&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; is honest about what it's going to do. The problem is it's also verbose, repetitive, and full of cosmetic changes (like recomputed tags) mixed in with real ones (like a database instance scheduled for &lt;code&gt;-/+ replace&lt;/code&gt;). On a 400-line plan, the dangerous changes hide.&lt;/p&gt;

&lt;p&gt;This is the kind of task AI is actually good at: skimming structured text, flagging the entries that matter, ignoring the rest. But "paste plan into Claude" is not the workflow. There's a specific shape to this that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why people get this wrong
&lt;/h2&gt;

&lt;p&gt;The natural instinct is to copy the plan output and paste it into a chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;Terraform&lt;/span&gt; &lt;span class="nx"&gt;will&lt;/span&gt; &lt;span class="nx"&gt;perform&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;following&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# aws_instance.web will be updated in-place&lt;/span&gt;
  &lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"i-0abc123def456"&lt;/span&gt;
      &lt;span class="err"&gt;~&lt;/span&gt; &lt;span class="nx"&gt;instance_type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.small"&lt;/span&gt; &lt;span class="nx"&gt;-&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"t3.medium"&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model will respond with a sentence about each line. You'll scroll. You'll skim. You'll miss the &lt;code&gt;-/+ replace&lt;/code&gt; on the database because it's in the middle of 30 routine updates.&lt;/p&gt;

&lt;p&gt;This is the same failure mode as pasting a wall of logs and asking "is anything wrong?" The model is too polite to skip things. You need to tell it to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format that actually works: JSON
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;terraform show -json tfplan&lt;/code&gt; outputs a structured representation of the plan that's much easier to reason about than the text format. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "actions" field is explicit.&lt;/strong&gt; Each resource_change has a &lt;code&gt;change.actions&lt;/code&gt; array — &lt;code&gt;["create"]&lt;/code&gt;, &lt;code&gt;["delete"]&lt;/code&gt;, &lt;code&gt;["update"]&lt;/code&gt;, or &lt;code&gt;["delete", "create"]&lt;/code&gt; for replace. No ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can filter before pasting.&lt;/strong&gt; With &lt;code&gt;jq&lt;/code&gt;, you can extract only the dangerous changes, drop the noise, and feed a 20-line summary into the AI instead of a 400-line plan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tfplan
terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; plan.json

&lt;span class="c"&gt;# Get just the dangerous changes&lt;/span&gt;
jq &lt;span class="s1"&gt;'[.resource_changes[] |
     select(.change.actions | contains(["delete"])) |
     {address, type, actions: .change.actions}]'&lt;/span&gt; plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the AI's input. Compact, unambiguous, and pre-filtered to the changes that need a human decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt that catches what plans hide
&lt;/h2&gt;

&lt;p&gt;Once you have the dangerous-changes JSON, the prompt is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's a Terraform plan summary showing only &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;replace&lt;/code&gt; operations. For each resource, tell me: (1) what data is at risk (none / state but not data / data + state), (2) what triggered the replacement if applicable (look at the change.before vs change.after for the field that forces new), (3) the recommended action — proceed, snapshot first, or block.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model now has a directed task with three clear outputs per finding. The response is scannable, actionable, and short.&lt;/p&gt;

&lt;p&gt;For comparison, the same prompt against the raw text plan produces a wandering essay.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;Here's a redacted plan summary from a recent change:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_instance.bastion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_instance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actions"&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="s2"&gt;"delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create"&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="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_db_instance.main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actions"&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="s2"&gt;"delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create"&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="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_s3_bucket.logs-old"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actions"&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="s2"&gt;"delete"&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="w"&gt;
&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;Claude's review of this (paraphrased):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_instance.bastion&lt;/code&gt;&lt;/strong&gt; — Replace is fine, no persistent data on bastions. Brief outage of jump-host access (~2 min). Proceed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_db_instance.main&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;DANGEROUS.&lt;/strong&gt; Database replace = data loss unless &lt;code&gt;skip_final_snapshot = false&lt;/code&gt; and you've verified the snapshot will be created. Check the plan JSON for &lt;code&gt;skip_final_snapshot&lt;/code&gt; in the &lt;code&gt;change.after&lt;/code&gt; — if true, BLOCK. If false, the snapshot will save data but restore is a manual operation. Recommend creating a manual snapshot first regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;aws_s3_bucket.logs-old&lt;/code&gt;&lt;/strong&gt; — Delete. If the bucket has objects, this fails by default. If &lt;code&gt;force_destroy = true&lt;/code&gt;, all objects are deleted with the bucket. Check the bucket isn't actively used.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly the kind of review I want before applying. Without the AI, I'd probably catch the DB replace, but I might miss the &lt;code&gt;force_destroy&lt;/code&gt; nuance on the S3 bucket because I'd be in a hurry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Sentinel / OPA / Checkov?
&lt;/h2&gt;

&lt;p&gt;These tools enforce &lt;em&gt;policies&lt;/em&gt; — "no public S3 buckets," "no RDS without deletion protection." They're floor-setting. They don't help with the per-change judgment calls: "is this specific replace acceptable for this specific resource right now?"&lt;/p&gt;

&lt;p&gt;I use both. Checkov in CI catches the consistent rule violations. AI review of the plan catches the contextual ones — the cases where a replace is technically allowed but operationally risky.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI integration
&lt;/h2&gt;

&lt;p&gt;Once this workflow proves out, you can automate it. A simple GitLab CI job:&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;plan-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:3.20&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apk add --no-cache jq curl&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DANGEROUS=$(jq '[.resource_changes[] |&lt;/span&gt;
                       &lt;span class="s"&gt;select(.change.actions | contains(["delete"]))]' plan.json)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;if [ "$(echo "$DANGEROUS" | jq length)" -gt 0 ]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Dangerous changes detected, requesting AI review..."&lt;/span&gt;
        &lt;span class="s"&gt;# Call Claude API with $DANGEROUS as input&lt;/span&gt;
        &lt;span class="s"&gt;# Post result as MR comment&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't quite production-ready in two paragraphs of YAML, but the pattern is: detect dangerous changes, send them to AI for contextual review, post the result where the human reviewer will see it.&lt;/p&gt;

&lt;p&gt;The point is to make the AI review part of the workflow, not a thing you remember to do. By the time you're tired enough to miss a replace in a 400-line plan, you also won't remember to ask AI about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI can't tell you about a plan
&lt;/h2&gt;

&lt;p&gt;It can't tell you whether the change is &lt;em&gt;intended&lt;/em&gt;. A replace of a database might be deliberate (you're migrating engines), in which case the snapshot-first advice is annoying overhead. The AI sees structure; you see intent. The two together is the workflow.&lt;/p&gt;

&lt;p&gt;It also can't tell you whether the change is &lt;em&gt;complete&lt;/em&gt;. Sometimes a plan looks safe in isolation but breaks something downstream because of a dependency you forgot about. The AI doesn't know your downstream dependencies. You do.&lt;/p&gt;

&lt;p&gt;The reviewer is still you. The AI is just a fast filter on the parts of the plan that need attention.&lt;/p&gt;

&lt;p&gt;For the full prompt set on Terraform safety, see the &lt;a href="https://dev.to/categories/terraform/"&gt;Terraform category&lt;/a&gt; — including &lt;a href="https://dev.to/prompts/terraform-plan-review-checklist/"&gt;terraform-plan-review-checklist&lt;/a&gt; and &lt;a href="https://dev.to/prompts/terraform-dangerous-changes-review/"&gt;terraform-dangerous-changes-review&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/ai-with-terraform-plans/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>ai</category>
      <category>plan</category>
    </item>
    <item>
      <title>DevOps as a Service Pricing: What Should Businesses Expect to Pay?</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Mon, 29 Jun 2026 22:02:07 +0000</pubDate>
      <link>https://dev.to/devopsaitoolkit/devops-as-a-service-pricing-what-should-businesses-expect-to-pay-2481</link>
      <guid>https://dev.to/devopsaitoolkit/devops-as-a-service-pricing-what-should-businesses-expect-to-pay-2481</guid>
      <description>&lt;p&gt;After 25 years of keeping production systems alive — building the automation, owning the pager, and helping companies stop bleeding money on preventable outages — the question I get asked most by founders and operations leads is blunt: &lt;em&gt;"What is this going to cost me?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is the one nobody likes: it depends. But "it depends" isn't useful if you're trying to budget. So let me give you the real version — what drives the number, the pricing models you'll actually be quoted, and a simple way to figure out whether the spend pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DevOps pricing varies so much
&lt;/h2&gt;

&lt;p&gt;There's no sticker price on DevOps for the same reason there's no sticker price on "fixing my house." A one-bedroom condo and a 40-year-old farmhouse are different jobs. Three things move the number more than anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Company size.&lt;/strong&gt; A two-person startup with one Linux server and a single web app is a fundamentally different engagement than a 200-person company running multiple Kubernetes clusters across regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure complexity.&lt;/strong&gt; A static site on a single cloud VM is cheap to run. A microservices platform with service meshes, multiple databases, message queues, and compliance requirements is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support expectations.&lt;/strong&gt; "Help us when something breaks during business hours" and "24/7 on-call with a 15-minute response SLA" are priced an order of magnitude apart, because one of them owns someone's nights and weekends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you can compare quotes, you have to be honest about which of those buckets you're actually in. A provider quoting you a low number may simply be assuming a smaller scope than the one you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common pricing models
&lt;/h2&gt;

&lt;p&gt;Most DevOps as a Service work is sold under one of five models. Each fits a different situation, and good providers will steer you toward the right one rather than forcing everything into their favorite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hourly / time-and-materials
&lt;/h3&gt;

&lt;p&gt;You pay for hours worked, usually billed against a monthly cap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When it fits:&lt;/strong&gt; Small, well-defined tasks, ad-hoc help, or an early relationship where neither side knows the full scope yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rough ballpark:&lt;/strong&gt; Rates vary widely by region and seniority. The trap is that hourly incentivizes activity, not outcomes — a cheap hourly rate from someone who takes three times as long is not a bargain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Monthly retainer
&lt;/h3&gt;

&lt;p&gt;A fixed monthly fee buys you a block of capacity and ongoing ownership of your infrastructure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When it fits:&lt;/strong&gt; You have living infrastructure that needs continuous care — patching, monitoring, upgrades, small improvements — and you want a predictable line item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example:&lt;/strong&gt; Ongoing Kubernetes version upgrades, Prometheus and Grafana tuning, and routine Ansible-driven patching of your Linux fleet are classic retainer work. The cluster doesn't stop needing attention, so neither does the engagement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project-based / fixed bid
&lt;/h3&gt;

&lt;p&gt;A scoped deliverable for a fixed price.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When it fits:&lt;/strong&gt; A clear, bounded build with a defined "done."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example:&lt;/strong&gt; A one-time Terraform plus GitLab CI/CD build-out — provision the cloud accounts, write the infrastructure as code, stand up the pipelines, Dockerize the apps, and hand it over — is naturally project-priced. You know what you're getting and what it costs before work starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Emergency / incident support
&lt;/h3&gt;

&lt;p&gt;On-demand help when production is on fire, often at a premium rate or via a pre-paid response retainer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When it fits:&lt;/strong&gt; You run your own systems day-to-day but want a number to call when something serious breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality check:&lt;/strong&gt; This is the most expensive way to buy help per hour, because you're paying for someone to drop everything. It's insurance, not a maintenance plan — and it's far cheaper to prevent the incident than to buy emergency labor mid-outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fully managed service
&lt;/h3&gt;

&lt;p&gt;The provider owns your DevOps function end to end — infrastructure, pipelines, monitoring, security, on-call, the lot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When it fits:&lt;/strong&gt; You don't want to hire and retain an internal platform team, or you want to extend the small one you have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality check:&lt;/strong&gt; This is the highest monthly spend, but compare it against the loaded cost of hiring senior engineers, the recruiting time, and the bus-factor risk of a one-person internal team. Often it's cheaper and far less fragile than building the same capability in-house.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A healthy engagement often mixes models: a project-priced initial build-out, then a monthly retainer to run what was built.&lt;/p&gt;

&lt;h2&gt;
  
  
  What services actually move the price
&lt;/h2&gt;

&lt;p&gt;Within any model, the scope of work is what sets the number. The big cost factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud setup and infrastructure as code.&lt;/strong&gt; Account structure, networking, and Terraform modules to make it all reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD pipelines.&lt;/strong&gt; Building and maintaining GitLab CI/CD (or equivalent) so deploys are fast, repeatable, and safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers and orchestration.&lt;/strong&gt; Docker images, registries, and Kubernetes — the single biggest complexity multiplier in modern infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring and observability.&lt;/strong&gt; Prometheus, Grafana, alerting rules, and dashboards. Good &lt;a href="https://dev.to/dashboard/monitoring-alerts/"&gt;monitoring and alert generation&lt;/a&gt; is what turns a 3am outage into a 9am ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; Secrets management, access control, network policy, vulnerability scanning, and hardening of your Linux servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups and disaster recovery.&lt;/strong&gt; Tested restores — not just backups that exist on paper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident response and on-call.&lt;/strong&gt; The cost of someone being awake and accountable when things go wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation.&lt;/strong&gt; Ansible playbooks and scripting that replace manual, error-prone toil.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; SOC 2, HIPAA, PCI, and friends add audit, documentation, and control work that materially raises cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more of these you need, and the higher the stakes, the higher the price. That's not padding — it's the actual work of keeping a real system running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cheaper is not always better
&lt;/h2&gt;

&lt;p&gt;Here's where my experience makes me opinionated: in production infrastructure, the cheapest quote is frequently the most expensive decision.&lt;/p&gt;

&lt;p&gt;A low bid usually means one of a few things — a junior engineer learning on your dime, a scope that quietly excludes monitoring or backups, or a contractor who'll bolt something together and disappear before the technical debt comes due. You don't find out until the pipeline breaks at the worst possible moment, the backups turn out to be untested, or a security gap becomes an incident.&lt;/p&gt;

&lt;p&gt;Infrastructure is one of those areas where you're not buying hours — you're buying the absence of disasters. That's hard to see on an invoice and very easy to feel in an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What downtime actually costs
&lt;/h2&gt;

&lt;p&gt;This is the framing that changes the conversation. Put a number on downtime and the "expensive" DevOps quote suddenly looks like a rounding error.&lt;/p&gt;

&lt;p&gt;A simple cost-of-downtime model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downtime cost per hour = (Annual revenue / Business hours per year) + recovery labor + reputation/churn cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Work a concrete example. Say a business does &lt;strong&gt;$5,000,000&lt;/strong&gt; in revenue a year and runs roughly &lt;strong&gt;3,000 business hours&lt;/strong&gt;. That's about &lt;strong&gt;$1,667 per hour&lt;/strong&gt; in direct lost revenue — before you add the engineers pulled off roadmap work to firefight, the customers who churn, and the support load from a public incident. Call it &lt;strong&gt;$2,500–$4,000 an hour&lt;/strong&gt;, conservatively.&lt;/p&gt;

&lt;p&gt;Now consider what causes that downtime in shops without proper DevOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Failed deployments&lt;/strong&gt; with no pipeline safeguards or rollback — a bad release that takes hours to unwind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor monitoring&lt;/strong&gt; that means you learn about the outage from angry customers instead of an alert, adding 30+ minutes of pure detection delay to every incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual, undocumented processes&lt;/strong&gt; where only one person knows how to restore the service, and they're on vacation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single multi-hour outage can cost more than a year of competent monitoring and incident-response coverage. The DevOps spend isn't competing with zero — it's competing with the outages it prevents.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI changes the math
&lt;/h2&gt;

&lt;p&gt;Part of why DevOps value-for-money has improved is that AI now removes a large slice of the repetitive labor that used to fill the bill.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drafting and reviewing infrastructure as code.&lt;/strong&gt; Terraform and Ansible scaffolding that used to take hours gets drafted in minutes, then reviewed by a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline and config generation.&lt;/strong&gt; GitLab CI/CD configs, Dockerfiles, and Kubernetes manifests start from a solid AI-generated baseline instead of a blank file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring setup.&lt;/strong&gt; Generating sensible Prometheus alert rules and Grafana panels — historically tedious, easily templated work — is far faster with AI assistance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident triage.&lt;/strong&gt; Summarizing logs and correlating "what changed" compresses the slow part of an outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key word is &lt;em&gt;assisted&lt;/em&gt; — a human still owns every change to production. But a provider using AI well can deliver more per dollar, which means you get broader coverage for the same budget. If you want to see the kind of work this accelerates, our &lt;a href="https://dev.to/prompts/"&gt;prompt library&lt;/a&gt; shows the patterns we lean on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting lean: startups and small businesses
&lt;/h2&gt;

&lt;p&gt;If you're early-stage, you do not need a fully managed enterprise engagement, and you shouldn't pay for one. Start with a lean package that covers the essentials and nothing you won't use yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A reproducible cloud setup with Terraform, so you're never clicking around a console by hand.&lt;/li&gt;
&lt;li&gt;One clean CI/CD pipeline so deploys are boring and repeatable.&lt;/li&gt;
&lt;li&gt;Basic monitoring and alerting on the handful of metrics that actually predict outages.&lt;/li&gt;
&lt;li&gt;Tested backups.&lt;/li&gt;
&lt;li&gt;A documented runbook so recovery doesn't depend on one person's memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a modest retainer or a small fixed-bid build-out, and it removes the failure modes that sink small companies. You add Kubernetes, deeper observability, and compliance work later — when the business actually needs them, not before. You can see how we structure tiers like this on our &lt;a href="https://dev.to/pricing"&gt;pricing&lt;/a&gt; page.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to calculate ROI
&lt;/h2&gt;

&lt;p&gt;Don't buy DevOps on vibes. Run the numbers. A usable formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROI (%) = ((Value gained - Cost of service) / Cost of service) x 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;strong&gt;value gained&lt;/strong&gt; is the sum of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Downtime avoided&lt;/strong&gt; — fewer outage hours × your cost-of-downtime-per-hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time reclaimed&lt;/strong&gt; — hours your developers stop spending on infrastructure toil, at their loaded cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster delivery&lt;/strong&gt; — features shipped sooner because the pipeline is fast and reliable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incidents prevented&lt;/strong&gt; — the emergency-rate firefighting you never have to buy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A worked example. Suppose a managed engagement costs &lt;strong&gt;$60,000 a year&lt;/strong&gt;. Over that year it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevents an estimated &lt;strong&gt;20 hours&lt;/strong&gt; of downtime at &lt;strong&gt;$3,000/hour&lt;/strong&gt; = &lt;strong&gt;$60,000&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Frees &lt;strong&gt;two developers&lt;/strong&gt; from ~5 hours/week of infra work — roughly &lt;strong&gt;$50,000&lt;/strong&gt; of reclaimed engineering time.&lt;/li&gt;
&lt;li&gt;Speeds delivery enough to pull in revenue you'd otherwise have deferred — call it &lt;strong&gt;$30,000&lt;/strong&gt;, conservatively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's &lt;strong&gt;$140,000&lt;/strong&gt; of value against &lt;strong&gt;$60,000&lt;/strong&gt; of cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROI = (($140,000 - $60,000) / $60,000) x 100 = ~133%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if you halve every one of those estimates to be safe, you're still solidly positive. The exercise matters more than the exact figures — when you actually price the downtime you avoid and the time you reclaim, good DevOps consistently pays for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;DevOps as a Service pricing genuinely varies, and any provider who hands you a flat number without understanding your systems is guessing. But the framework is straightforward: know which size and complexity bucket you're in, pick the pricing model that fits the work, scope the services you actually need, and run the ROI math against the very real cost of doing nothing.&lt;/p&gt;

&lt;p&gt;The mistake I see most often is treating DevOps as a cost line to minimize. It isn't. It's an investment in uptime, delivery speed, security, and the ability to scale without setting your infrastructure on fire. Price it against the outages, the lost engineering hours, and the deals you can't close because the platform won't hold — and the question stops being "what does this cost?" and becomes "what is it costing me not to have it?"&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost figures and ranges here are illustrative. Build your own estimate from your real revenue, infrastructure, and risk profile before committing to a budget.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://devopsaitoolkit.com/blog/devops-as-a-service-pricing-what-to-expect/" rel="noopener noreferrer"&gt;DevOps AI ToolKit&lt;/a&gt; — practical AI workflows for cloud engineers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>pricing</category>
      <category>manageddevops</category>
      <category>roi</category>
    </item>
  </channel>
</rss>
