<?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: Satyaki</title>
    <description>The latest articles on DEV Community by Satyaki (@blackzu).</description>
    <link>https://dev.to/blackzu</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F92977%2F8e14d52c-e9f3-4c96-9ec5-3e4fdc11018a.png</url>
      <title>DEV Community: Satyaki</title>
      <link>https://dev.to/blackzu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blackzu"/>
    <language>en</language>
    <item>
      <title>Image Volume Type GA in Kubernetes 1.36 — Finally Killing the Init Container Copy Pattern</title>
      <dc:creator>Satyaki</dc:creator>
      <pubDate>Thu, 21 May 2026 05:49:06 +0000</pubDate>
      <link>https://dev.to/blackzu/image-volume-type-ga-in-kubernetes-136-finally-killing-the-init-container-copy-pattern-182k</link>
      <guid>https://dev.to/blackzu/image-volume-type-ga-in-kubernetes-136-finally-killing-the-init-container-copy-pattern-182k</guid>
      <description>&lt;p&gt;For years, Kubernetes engineers have used the same awkward pattern whenever an application needed large read-only assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Init container&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emptyDir&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cp -r&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wait for startup&lt;/li&gt;
&lt;li&gt;Duplicate storage usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It worked, but it always felt like a workaround rather than a first-class Kubernetes primitive.&lt;/p&gt;

&lt;p&gt;With Kubernetes 1.36, the &lt;code&gt;image&lt;/code&gt; volume type is now GA, and it fundamentally changes how Pods can consume immutable file bundles.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pulling files into an init container,&lt;/li&gt;
&lt;li&gt;copying them into an &lt;code&gt;emptyDir&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;and mounting them into the main container,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes can now directly mount an OCI image filesystem into a container as a read-only volume.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no init-container copy step,&lt;/li&gt;
&lt;li&gt;no duplicated bytes on disk,&lt;/li&gt;
&lt;li&gt;faster startup times,&lt;/li&gt;
&lt;li&gt;smaller artifact images,&lt;/li&gt;
&lt;li&gt;and much cleaner Pod specs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This becomes especially powerful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ML models&lt;/li&gt;
&lt;li&gt;static websites&lt;/li&gt;
&lt;li&gt;WASM modules&lt;/li&gt;
&lt;li&gt;OPA bundles&lt;/li&gt;
&lt;li&gt;language packs&lt;/li&gt;
&lt;li&gt;Grafana dashboards&lt;/li&gt;
&lt;li&gt;plugin distributions&lt;/li&gt;
&lt;li&gt;and any independently versioned read-only asset bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me walk through a realistic end-to-end scenario.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Scenario
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Team A owns nginx (the web server).&lt;/li&gt;
&lt;li&gt;Team B owns the website content (HTML/CSS/JS).&lt;/li&gt;
&lt;li&gt;Team B ships new content 5x/day.&lt;/li&gt;
&lt;li&gt;Team A ships nginx config changes maybe once a month.&lt;/li&gt;
&lt;li&gt;They should not be coupled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The static assets live in an OCI image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;registry.example.com/web/site-assets:v42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This image is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scratch + files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No shell. No entrypoint. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/site/index.html
/site/style.css
/site/app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  The Old Way (Pre-1.36): Init Container + emptyDir
&lt;/h1&gt;

&lt;h2&gt;
  
  
  How You Built the Assets Image
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile for site-assets&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;busybox:1.36&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;source&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./site /site&lt;/span&gt;

&lt;span class="c"&gt;# Final image needs a shell because the init container will run `cp`&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; busybox:1.36&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=source /site /site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice something important:&lt;/p&gt;

&lt;p&gt;You cannot use &lt;code&gt;FROM scratch&lt;/code&gt; here because the init container needs tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the image is bloated with BusyBox purely to enable the copy operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pod Manifest
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web-old-way&lt;/span&gt;

&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regcred&lt;/span&gt;

  &lt;span class="na"&gt;initContainers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;load-assets&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;registry.example.com/web/site-assets:v42&lt;/span&gt;

      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sh&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cp -r /site/* /shared/&lt;/span&gt;

      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/shared&lt;/span&gt;

  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&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;nginx:1.27&lt;/span&gt;

      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nginx/html&lt;/span&gt;
          &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Actually Happening Under the Hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Images are pulled
&lt;/h3&gt;

&lt;p&gt;Kubelet pulls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nginx:1.27&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;site-assets:v42&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;using the Pod's &lt;code&gt;imagePullSecrets&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Kubelet creates the emptyDir
&lt;/h3&gt;

&lt;p&gt;Kubernetes creates an actual directory on the node filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/kubelet/pods/&amp;lt;pod-uid&amp;gt;/volumes/kubernetes.io~empty-dir/shared/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point it is completely empty.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Init container starts
&lt;/h3&gt;

&lt;p&gt;The init container's root filesystem is the &lt;code&gt;site-assets&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;emptyDir&lt;/code&gt; gets bind-mounted into the init container at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. The copy operation happens
&lt;/h3&gt;

&lt;p&gt;The init container executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /site/&lt;span class="k"&gt;*&lt;/span&gt; /shared/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every byte gets physically copied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image layer -&amp;gt; emptyDir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. Init container exits
&lt;/h3&gt;

&lt;p&gt;Kubelet records successful completion.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. nginx starts
&lt;/h3&gt;

&lt;p&gt;The same &lt;code&gt;emptyDir&lt;/code&gt; is mounted into nginx at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nginx now serves the copied files.&lt;/p&gt;




&lt;h1&gt;
  
  
  Real Problems With the Old Pattern
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Disk Usage Doubles
&lt;/h2&gt;

&lt;p&gt;The files now exist in two places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/lib/containerd/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AND&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;emptyDir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 2 GB ML model becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 GB image layer + 2 GB copy = 4 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;per Pod.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Startup Latency
&lt;/h2&gt;

&lt;p&gt;Large copy operations are expensive.&lt;/p&gt;

&lt;p&gt;A 2 GB copy operation can easily add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10–30 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before the main container even starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. You Need a Shell
&lt;/h2&gt;

&lt;p&gt;You cannot use:&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; scratch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because the image needs tooling like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;larger images&lt;/li&gt;
&lt;li&gt;more CVEs&lt;/li&gt;
&lt;li&gt;more attack surface&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Verbose YAML
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;init containers&lt;/li&gt;
&lt;li&gt;shared volumes&lt;/li&gt;
&lt;li&gt;multiple mounts&lt;/li&gt;
&lt;li&gt;copy commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All just to move files around.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. No Sharing Across Pods
&lt;/h2&gt;

&lt;p&gt;Every Pod independently copies the same bytes into its own &lt;code&gt;emptyDir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ten Pods on one node means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 independent copies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  The New Way (Kubernetes 1.36): Image Volume Type
&lt;/h1&gt;

&lt;h2&gt;
  
  
  How You Build the Assets Image Now
&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;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./site /site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No shell.&lt;br&gt;
No BusyBox.&lt;br&gt;
No executables.&lt;/p&gt;

&lt;p&gt;Just files.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Pod Manifest
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;

&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web-new-way&lt;/span&gt;

&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regcred&lt;/span&gt;

  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&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;nginx:1.27&lt;/span&gt;

      &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;assets&lt;/span&gt;
          &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/share/nginx/html&lt;/span&gt;
          &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;site&lt;/span&gt;

  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;assets&lt;/span&gt;

      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/web/site-assets:v42&lt;/span&gt;
        &lt;span class="na"&gt;pullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No init container.&lt;br&gt;
No &lt;code&gt;emptyDir&lt;/code&gt;.&lt;br&gt;
No copy operation.&lt;/p&gt;


&lt;h1&gt;
  
  
  What's Actually Happening Under the Hood Now
&lt;/h1&gt;
&lt;h2&gt;
  
  
  1. Kubelet sees an image volume
&lt;/h2&gt;

&lt;p&gt;Kubelet notices:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and asks the CRI runtime to mount the image.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Runtime pulls the image
&lt;/h2&gt;

&lt;p&gt;containerd or CRI-O pulls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;site-assets:v42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using the normal image pull pipeline.&lt;/p&gt;

&lt;p&gt;Exactly the same mechanism used for container images.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Runtime unpacks image layers
&lt;/h2&gt;

&lt;p&gt;The image gets unpacked into the runtime snapshot store.&lt;/p&gt;

&lt;p&gt;For example with containerd:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;overlayfs snapshotter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No container is started.&lt;/p&gt;

&lt;p&gt;Only the filesystem is materialized.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Filesystem is bind-mounted directly
&lt;/h2&gt;

&lt;p&gt;The runtime bind-mounts the image filesystem directly into the nginx container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read-only by design.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. nginx starts immediately
&lt;/h2&gt;

&lt;p&gt;No copy step.&lt;br&gt;
No waiting.&lt;/p&gt;

&lt;p&gt;The files already exist.&lt;/p&gt;


&lt;h1&gt;
  
  
  The Mental Model Shift
&lt;/h1&gt;

&lt;p&gt;The old model was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Start a helper container and copy files somewhere."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The new model is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Mount an OCI image filesystem directly as a volume."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds subtle, but architecturally it's a major shift.&lt;/p&gt;

&lt;p&gt;Kubernetes is effectively treating OCI images as generic immutable data artifacts — not just runnable containers.&lt;/p&gt;


&lt;h1&gt;
  
  
  What You Gain
&lt;/h1&gt;
&lt;h2&gt;
  
  
  No Data Duplication
&lt;/h2&gt;

&lt;p&gt;The image is bind-mounted directly.&lt;/p&gt;

&lt;p&gt;No copy operation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Faster Startup
&lt;/h2&gt;

&lt;p&gt;You eliminate the init-container copy phase entirely.&lt;/p&gt;

&lt;p&gt;For large datasets or ML models, this is massive.&lt;/p&gt;


&lt;h2&gt;
  
  
  Smaller and Safer Images
&lt;/h2&gt;

&lt;p&gt;You can now use:&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; scratch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;smaller images&lt;/li&gt;
&lt;li&gt;fewer CVEs&lt;/li&gt;
&lt;li&gt;reduced attack surface&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Always Read-Only
&lt;/h2&gt;

&lt;p&gt;Image volumes are immutable by specification.&lt;/p&gt;

&lt;p&gt;The runtime enforces it.&lt;/p&gt;

&lt;p&gt;Applications cannot modify the mounted content.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shared Across Pods
&lt;/h2&gt;

&lt;p&gt;Ten Pods mounting the same image on the same node share the same underlying bytes.&lt;/p&gt;

&lt;p&gt;Huge improvement for large artifact distribution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cleaner YAML
&lt;/h2&gt;

&lt;p&gt;The Pod spec now clearly expresses intent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Mount this image's filesystem here."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;instead of implementing an entire file-copy workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Important Caveats
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Image Volumes Are Always Read-Only
&lt;/h2&gt;

&lt;p&gt;If your application needs writable storage, use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;emptyDir&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PVCs&lt;/li&gt;
&lt;li&gt;ephemeral storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  subPath Is Extremely Useful
&lt;/h2&gt;

&lt;p&gt;If your files live under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside the image but you want them mounted directly into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&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;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;solves that cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  pullPolicy Works Exactly Like Container Images
&lt;/h2&gt;

&lt;p&gt;You can use:&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;pullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&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;pullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;exactly as you already do with containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  No Environment Variable Substitution
&lt;/h2&gt;

&lt;p&gt;This does NOT work:&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;reference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ASSET_VERSION}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field is literal.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helm&lt;/li&gt;
&lt;li&gt;Kustomize&lt;/li&gt;
&lt;li&gt;templating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Pods Don't Automatically Update
&lt;/h2&gt;

&lt;p&gt;If somebody pushes a new image to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:v42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;existing Pods continue using the old mounted bytes.&lt;/p&gt;

&lt;p&gt;You must roll the Pod to pick up changes.&lt;/p&gt;

&lt;p&gt;Which is good for reproducibility.&lt;/p&gt;

&lt;p&gt;In production, pin image digests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Runtime Support Matters
&lt;/h2&gt;

&lt;p&gt;You need modern runtimes.&lt;/p&gt;

&lt;p&gt;Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;containerd &amp;gt;= 2.1&lt;/li&gt;
&lt;li&gt;CRI-O &amp;gt;= 1.31&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Older runtimes will fail with a clear unsupported feature error.&lt;/p&gt;




&lt;h1&gt;
  
  
  How imagePullSecrets Work
&lt;/h1&gt;

&lt;p&gt;This is one of the nicest parts.&lt;/p&gt;

&lt;p&gt;Image volumes automatically use the same authentication flow as normal container images.&lt;/p&gt;

&lt;p&gt;That means Kubernetes automatically uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pod &lt;code&gt;imagePullSecrets&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ServiceAccount &lt;code&gt;imagePullSecrets&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;kubelet credential providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No additional auth wiring required.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regcred&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;works for BOTH:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;container images&lt;/li&gt;
&lt;li&gt;image volumes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Multiple Private Registries
&lt;/h2&gt;

&lt;p&gt;If assets and application images live in different registries:&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;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-registry-creds&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;assets-registry-creds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime tries the secrets in order and uses whichever matches the registry hostname.&lt;/p&gt;




&lt;h1&gt;
  
  
  Quick Comparison
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Init Container + emptyDir&lt;/th&gt;
&lt;th&gt;Image Volume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod complexity&lt;/td&gt;
&lt;td&gt;Multiple containers and mounts&lt;/td&gt;
&lt;td&gt;Single volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assets image&lt;/td&gt;
&lt;td&gt;Needs shell/cp&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FROM scratch&lt;/code&gt; works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk usage&lt;/td&gt;
&lt;td&gt;Image + copied bytes&lt;/td&gt;
&lt;td&gt;Image only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup time&lt;/td&gt;
&lt;td&gt;Pull + copy&lt;/td&gt;
&lt;td&gt;Pull only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writable&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharing across Pods&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imagePullSecrets&lt;/td&gt;
&lt;td&gt;Pod spec&lt;/td&gt;
&lt;td&gt;Pod spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update without restart&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes support&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;1.31 alpha → 1.33 beta → 1.36 GA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  When You Should Actually Use It
&lt;/h1&gt;

&lt;p&gt;Image volumes are ideal when you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large read-only assets&lt;/li&gt;
&lt;li&gt;independently versioned bundles&lt;/li&gt;
&lt;li&gt;OCI-distributed artifacts&lt;/li&gt;
&lt;li&gt;data shared across multiple Pods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ML models&lt;/li&gt;
&lt;li&gt;static websites&lt;/li&gt;
&lt;li&gt;OPA bundles&lt;/li&gt;
&lt;li&gt;plugins&lt;/li&gt;
&lt;li&gt;WASM modules&lt;/li&gt;
&lt;li&gt;Grafana dashboards&lt;/li&gt;
&lt;li&gt;language packs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the artifacts are too large for ConfigMaps&lt;/li&gt;
&lt;li&gt;you want registry-native distribution&lt;/li&gt;
&lt;li&gt;you want image signing/scanning/RBAC&lt;/li&gt;
&lt;li&gt;the content must remain immutable&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  When NOT To Use It
&lt;/h1&gt;

&lt;p&gt;Don't use image volumes when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the application needs writable storage&lt;/li&gt;
&lt;li&gt;the content is tiny text configuration&lt;/li&gt;
&lt;li&gt;the data is stateful per Pod&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ConfigMaps&lt;/li&gt;
&lt;li&gt;PVCs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;emptyDir&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are still better fits.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;image&lt;/code&gt; volume type feels small on paper, but it removes one of the longest-standing operational hacks in Kubernetes.&lt;/p&gt;

&lt;p&gt;For years, platform engineers built elaborate init-container copy workflows just to move immutable files into Pods.&lt;/p&gt;

&lt;p&gt;Now Kubernetes finally has a native primitive for it.&lt;/p&gt;

&lt;p&gt;If your workloads distribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large read-only assets&lt;/li&gt;
&lt;li&gt;ML models&lt;/li&gt;
&lt;li&gt;frontend bundles&lt;/li&gt;
&lt;li&gt;policy packs&lt;/li&gt;
&lt;li&gt;plugins&lt;/li&gt;
&lt;li&gt;shared runtime data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;this feature can significantly reduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;startup latency&lt;/li&gt;
&lt;li&gt;storage duplication&lt;/li&gt;
&lt;li&gt;image complexity&lt;/li&gt;
&lt;li&gt;YAML noise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, it aligns Kubernetes with a broader industry shift:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OCI images are no longer just executable containers.&lt;br&gt;
They're becoming the standard distribution format for software artifacts in general.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And image volumes push Kubernetes one step further in that direction.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>containers</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>CPU Humbled Me — A Kubernetes Throttling Story Hidden Between Prometheus Scrapes</title>
      <dc:creator>Satyaki</dc:creator>
      <pubDate>Fri, 15 May 2026 15:12:32 +0000</pubDate>
      <link>https://dev.to/blackzu/cpu-humbled-me-a-kubernetes-throttling-story-hidden-between-prometheus-scrapes-4ah8</link>
      <guid>https://dev.to/blackzu/cpu-humbled-me-a-kubernetes-throttling-story-hidden-between-prometheus-scrapes-4ah8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Memory is easy. CPU humbled me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With memory, the rule is brutal but clear — cross the limit, the pod gets OOMKilled. Done.&lt;/p&gt;

&lt;p&gt;CPU? CPU is sneaky. And I ignored it for the longest time… until it broke production.&lt;/p&gt;

&lt;p&gt;Here's what happened 👇&lt;/p&gt;

&lt;p&gt;We had an app running peacefully in-house. Then it went client-facing. Traffic surged, and suddenly ~15% of requests started timing out — most of them on DB calls.&lt;/p&gt;

&lt;p&gt;I opened Grafana expecting a smoking gun. Nothing. CPU usage looked "fine." No throttling alerts screaming at me. Just confused timeouts.&lt;/p&gt;

&lt;p&gt;The trap? &lt;strong&gt;Throttling happens in milliseconds. Prometheus scrapes every 15 seconds.&lt;/strong&gt; Every bit of evidence was hiding between the scrapes.&lt;/p&gt;

&lt;p&gt;Here was the setup:&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;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;800m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.5Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numbers from the incident (rough, but directionally honest):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normal:&lt;/strong&gt; 300 req/min → avg CPU ~180m&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surge:&lt;/strong&gt; 1200 req/min → avg CPU ~650m, ~15% timeouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I sat down and actually did the math instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How CPU actually works
&lt;/h2&gt;

&lt;p&gt;CPU is compressible. Memory isn't. When CPU runs out, your process doesn't die — it gets &lt;em&gt;throttled&lt;/em&gt;. The Linux CFS scheduler slices time into periods (default: &lt;strong&gt;100ms&lt;/strong&gt;). Within each period, your container gets a quota based on its limit. Cross the quota mid-period? You wait for the next one. That wait &lt;em&gt;is&lt;/em&gt; the latency you're seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking through the numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Normal load:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;300 req/min = 5 req/sec = 0.5 requests per 100ms
Avg CPU 180m = 18ms of CPU work per 100ms period
→ 18ms ÷ 0.5 req = ~36ms of CPU work per request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Surge load:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1200 req/min = 20 req/sec = 2 requests per 100ms
2 × 36ms = 72ms of CPU work needed per 100ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the limit was 800m → &lt;strong&gt;80ms quota per 100ms&lt;/strong&gt;. Looks fine on paper, right?&lt;/p&gt;

&lt;p&gt;Here's the catch: avg CPU was 650m (65ms). The &lt;em&gt;average&lt;/em&gt; hides the bursts. Some periods sat well below quota; others blew past the 80ms ceiling and got throttled. Average everything out across 15s scrapes and the dashboard whispers "all good" while users get timeouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the lesson.&lt;/strong&gt; Average CPU is a liar in bursty workloads. Throttling lives in the gaps your monitoring can't see.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually look at
&lt;/h2&gt;

&lt;p&gt;Stop staring at &lt;code&gt;container_cpu_usage_seconds_total&lt;/code&gt;. Look at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;container_cpu_cfs_throttled_periods_total&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;container_cpu_cfs_throttled_seconds_total&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ratio of throttled periods to total periods tells you the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remediations (in order of maturity, not just "increase the limit")
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Right-size first.&lt;/strong&gt; Requests and limits should reflect real workload behavior, not guesses copy-pasted from a template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load test before going client-facing.&lt;/strong&gt; Running an app in-house ≠ serving real traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPA recommendations&lt;/strong&gt; to understand what the app actually wants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HPA&lt;/strong&gt; so bursts get distributed across replicas instead of crushing one pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then, if needed, raise the limit&lt;/strong&gt; — with intent, not panic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bumping the limit is the easiest fix and the most expensive habit. Every patch carries a hidden cost — node capacity, bin-packing, cluster bills, blast radius. Understand the &lt;em&gt;why&lt;/em&gt; before you reach for the YAML.&lt;/p&gt;




&lt;p&gt;This one incident taught me more about Kubernetes resource management than months of reading docs. If you're running anything client-facing, please don't wait for a production incident to learn this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU isn't just a number on a dashboard. It's a time budget — and your users feel every millisecond you overspend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you been burned by CFS throttling? What metric finally gave it away for you? Drop it in the comments — I'd love to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>sre</category>
      <category>observability</category>
    </item>
    <item>
      <title>Understanding Kube-proxy &amp; CoreDNS in Kubernetes no bluff</title>
      <dc:creator>Satyaki</dc:creator>
      <pubDate>Thu, 22 Jan 2026 15:23:21 +0000</pubDate>
      <link>https://dev.to/blackzu/understanding-kube-proxy-coredns-in-kubernetes-no-bluff-23bc</link>
      <guid>https://dev.to/blackzu/understanding-kube-proxy-coredns-in-kubernetes-no-bluff-23bc</guid>
      <description>&lt;p&gt;🛠 Setting the Stage: A Kind Cluster&lt;/p&gt;

&lt;p&gt;Kubernetes is full of magic, but one of its most fascinating components is kube-proxy. It’s the silent operator that ensures traffic hitting a Service gets distributed across the right Pods. Under the hood, kube-proxy leverages Linux iptables to make this happen. Let’s peel back the layers and see it in action.&lt;/p&gt;

&lt;p&gt;For this demo, I spun up a 3-node Kind cluster. On top of it, I deployed a simple nginx Deployment exposed via a ClusterIP Service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uk84g5ojzvn24hvx408.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9uk84g5ojzvn24hvx408.png" alt=" " width="800" height="48"&gt;&lt;/a&gt;&lt;br&gt;
Here’s the deployment and service in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5jq98x81jx6sq0ezoli.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn5jq98x81jx6sq0ezoli.png" alt=" " width="800" height="65"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📜 Peeking into iptables&lt;/p&gt;

&lt;p&gt;Now comes the fun part. I logged into one of the nodes where a Pod is running and listed the NAT rules in the KUBE-SERVICES chain:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgtit5b6cok39fcyuj7d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgtit5b6cok39fcyuj7d.png" alt=" " width="800" height="86"&gt;&lt;/a&gt;&lt;br&gt;
Notice the entry for our nginx-deployment Service. The destination IP here is the ClusterIP of the Service. This is kube-proxy’s starting point for redirecting traffic&lt;/p&gt;

&lt;p&gt;🔀 Diving into the Service Chain&lt;/p&gt;

&lt;p&gt;Every Service gets its own chain. For nginx, that’s KUBE-SVC-WRNOD73BKRQH4VVX. Let’s inspect it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ge4gfetafngmbrj0xmw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ge4gfetafngmbrj0xmw.png" alt=" " width="800" height="66"&gt;&lt;/a&gt;&lt;br&gt;
And here’s the magic:&lt;br&gt;
When traffic hits the ClusterIP, kube-proxy rewrites it to one of the Pod IPs backing the Deployment.&lt;br&gt;
The rules show a probability ratio — in this case, 50/50. That means half the traffic goes to one Pod, and the other half to the second Pod.&lt;br&gt;
This is how kube-proxy achieves load balancing using nothing more than iptables.&lt;br&gt;
So, what did we just see?&lt;/p&gt;

&lt;p&gt;ClusterIP → Pod IPs translation via iptables.&lt;br&gt;
Masquerading ensures the source IP is rewritten correctly.&lt;br&gt;
Probability rules distribute traffic evenly across endpoints&lt;/p&gt;

&lt;p&gt;🌐 How DNS Works in the Cluster&lt;/p&gt;

&lt;p&gt;So far, we’ve seen how kube-proxy handles traffic routing and load balancing. But how does your application even know where to send requests? That’s where CoreDNS comes in.&lt;br&gt;
CoreDNS acts as the nameserver inside Kubernetes, resolving Service names into their corresponding ClusterIPs. Let’s walk through it step by step.&lt;/p&gt;

&lt;p&gt;🔍 Inspecting the kube-dns Service&lt;/p&gt;

&lt;p&gt;In the kube-system namespace, you’ll find the kube-dns Service. This is essentially the front door to CoreDNS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbkbhtfrcmu171avcrp6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbkbhtfrcmu171avcrp6u.png" alt=" " width="800" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📄 The resolv.conf File&lt;/p&gt;

&lt;p&gt;Inside Pods, the resolv.conf file contains the nameserver details and DNS search domains. This is how Kubernetes ensures that when you query something like nginx-deployment.default.svc.cluster.local, it knows how to resolve it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz598u8m5l5i21bint9vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz598u8m5l5i21bint9vt.png" alt=" " width="738" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🧪 Testing with nslookup&lt;/p&gt;

&lt;p&gt;Let’s put it to the test. Logging into a node and running an nslookup shows the DNS resolution in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnog9vvihm632zdd02kyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnog9vvihm632zdd02kyk.png" alt=" " width="614" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it works exactly as expected — the Service name resolves to the ClusterIP, which kube-proxy then maps to the Pod IPs.&lt;/p&gt;

&lt;p&gt;🎯 Wrapping It All Up&lt;/p&gt;

&lt;p&gt;Between kube-proxy and CoreDNS, Kubernetes ensures that:&lt;/p&gt;

&lt;p&gt;Traffic hitting a Service is load balanced across Pods.&lt;br&gt;
Service names are resolved seamlessly into ClusterIPs.&lt;br&gt;
Applications don’t need to worry about IP addresses — they just use DNS names. These two components are the backbone of Kubernetes networking. Without them, Services wouldn’t be discoverable or scalable.&lt;br&gt;
🔥 And that’s the no-bluff walkthrough of kube-proxy and CoreDNS — two vital pieces of the Kubernetes puzzle. Next time you deploy an app, you’ll know exactly how the traffic finds its way to the right Pod.&lt;/p&gt;

&lt;p&gt;Thats what kube-proxy does. Isnt it really cool ? &lt;/p&gt;

</description>
      <category>devops</category>
      <category>networking</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
