DEV Community

Satyaki
Satyaki

Posted on

Image Volume Type GA in Kubernetes 1.36 — Finally Killing the Init Container Copy Pattern

For years, Kubernetes engineers have used the same awkward pattern whenever an application needed large read-only assets:

  • Init container
  • emptyDir
  • cp -r
  • Wait for startup
  • Duplicate storage usage

It worked, but it always felt like a workaround rather than a first-class Kubernetes primitive.

With Kubernetes 1.36, the image volume type is now GA, and it fundamentally changes how Pods can consume immutable file bundles.

Instead of:

  • pulling files into an init container,
  • copying them into an emptyDir,
  • and mounting them into the main container,

Kubernetes can now directly mount an OCI image filesystem into a container as a read-only volume.

That means:

  • no init-container copy step,
  • no duplicated bytes on disk,
  • faster startup times,
  • smaller artifact images,
  • and much cleaner Pod specs.

This becomes especially powerful for:

  • ML models
  • static websites
  • WASM modules
  • OPA bundles
  • language packs
  • Grafana dashboards
  • plugin distributions
  • and any independently versioned read-only asset bundle

Let me walk through a realistic end-to-end scenario.


The Scenario

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

The static assets live in an OCI image:

registry.example.com/web/site-assets:v42
Enter fullscreen mode Exit fullscreen mode

This image is:

scratch + files
Enter fullscreen mode Exit fullscreen mode

No shell. No entrypoint. Just:

/site/index.html
/site/style.css
/site/app.js
Enter fullscreen mode Exit fullscreen mode

The Old Way (Pre-1.36): Init Container + emptyDir

How You Built the Assets Image

# Dockerfile for site-assets

FROM busybox:1.36 AS source
COPY ./site /site

# Final image needs a shell because the init container will run `cp`
FROM busybox:1.36
COPY --from=source /site /site
Enter fullscreen mode Exit fullscreen mode

Notice something important:

You cannot use FROM scratch here because the init container needs tools like:

  • cp
  • sh

So the image is bloated with BusyBox purely to enable the copy operation.


The Pod Manifest

apiVersion: v1
kind: Pod
metadata:
  name: web-old-way

spec:
  imagePullSecrets:
    - name: regcred

  initContainers:
    - name: load-assets
      image: registry.example.com/web/site-assets:v42

      command:
        - sh
        - -c
        - cp -r /site/* /shared/

      volumeMounts:
        - name: shared
          mountPath: /shared

  containers:
    - name: nginx
      image: nginx:1.27

      volumeMounts:
        - name: shared
          mountPath: /usr/share/nginx/html
          readOnly: true

  volumes:
    - name: shared
      emptyDir: {}
Enter fullscreen mode Exit fullscreen mode

What's Actually Happening Under the Hood

1. Images are pulled

Kubelet pulls:

  • nginx:1.27
  • site-assets:v42

using the Pod's imagePullSecrets.


2. Kubelet creates the emptyDir

Kubernetes creates an actual directory on the node filesystem:

/var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~empty-dir/shared/
Enter fullscreen mode Exit fullscreen mode

At this point it is completely empty.


3. Init container starts

The init container's root filesystem is the site-assets image.

The emptyDir gets bind-mounted into the init container at:

/shared
Enter fullscreen mode Exit fullscreen mode

4. The copy operation happens

The init container executes:

cp -r /site/* /shared/
Enter fullscreen mode Exit fullscreen mode

Every byte gets physically copied:

image layer -> emptyDir
Enter fullscreen mode Exit fullscreen mode

5. Init container exits

Kubelet records successful completion.


6. nginx starts

The same emptyDir is mounted into nginx at:

/usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

nginx now serves the copied files.


Real Problems With the Old Pattern

1. Disk Usage Doubles

The files now exist in two places:

/var/lib/containerd/...
Enter fullscreen mode Exit fullscreen mode

AND

emptyDir
Enter fullscreen mode Exit fullscreen mode

A 2 GB ML model becomes:

2 GB image layer + 2 GB copy = 4 GB
Enter fullscreen mode Exit fullscreen mode

per Pod.


2. Startup Latency

Large copy operations are expensive.

A 2 GB copy operation can easily add:

10–30 seconds
Enter fullscreen mode Exit fullscreen mode

before the main container even starts.


3. You Need a Shell

You cannot use:

FROM scratch
Enter fullscreen mode Exit fullscreen mode

because the image needs tooling like:

  • cp
  • sh

That means:

  • larger images
  • more CVEs
  • more attack surface

4. Verbose YAML

You need:

  • init containers
  • shared volumes
  • multiple mounts
  • copy commands

All just to move files around.


5. No Sharing Across Pods

Every Pod independently copies the same bytes into its own emptyDir.

Ten Pods on one node means:

10 independent copies
Enter fullscreen mode Exit fullscreen mode

The New Way (Kubernetes 1.36): Image Volume Type

How You Build the Assets Image Now

FROM scratch

COPY ./site /site
Enter fullscreen mode Exit fullscreen mode

That's it.

No shell.
No BusyBox.
No executables.

Just files.


The Pod Manifest

apiVersion: v1
kind: Pod

metadata:
  name: web-new-way

spec:
  imagePullSecrets:
    - name: regcred

  containers:
    - name: nginx
      image: nginx:1.27

      volumeMounts:
        - name: assets
          mountPath: /usr/share/nginx/html
          subPath: site

  volumes:
    - name: assets

      image:
        reference: registry.example.com/web/site-assets:v42
        pullPolicy: IfNotPresent
Enter fullscreen mode Exit fullscreen mode

No init container.
No emptyDir.
No copy operation.


What's Actually Happening Under the Hood Now

1. Kubelet sees an image volume

Kubelet notices:

volumes:
  - image:
Enter fullscreen mode Exit fullscreen mode

and asks the CRI runtime to mount the image.


2. Runtime pulls the image

containerd or CRI-O pulls:

site-assets:v42
Enter fullscreen mode Exit fullscreen mode

using the normal image pull pipeline.

Exactly the same mechanism used for container images.


3. Runtime unpacks image layers

The image gets unpacked into the runtime snapshot store.

For example with containerd:

overlayfs snapshotter
Enter fullscreen mode Exit fullscreen mode

No container is started.

Only the filesystem is materialized.


4. Filesystem is bind-mounted directly

The runtime bind-mounts the image filesystem directly into the nginx container:

/usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

Read-only by design.


5. nginx starts immediately

No copy step.
No waiting.

The files already exist.


The Mental Model Shift

The old model was:

"Start a helper container and copy files somewhere."

The new model is:

"Mount an OCI image filesystem directly as a volume."

That sounds subtle, but architecturally it's a major shift.

Kubernetes is effectively treating OCI images as generic immutable data artifacts — not just runnable containers.


What You Gain

No Data Duplication

The image is bind-mounted directly.

No copy operation.


Faster Startup

You eliminate the init-container copy phase entirely.

For large datasets or ML models, this is massive.


Smaller and Safer Images

You can now use:

FROM scratch
Enter fullscreen mode Exit fullscreen mode

which means:

  • smaller images
  • fewer CVEs
  • reduced attack surface

Always Read-Only

Image volumes are immutable by specification.

The runtime enforces it.

Applications cannot modify the mounted content.


Shared Across Pods

Ten Pods mounting the same image on the same node share the same underlying bytes.

Huge improvement for large artifact distribution.


Cleaner YAML

The Pod spec now clearly expresses intent:

"Mount this image's filesystem here."

instead of implementing an entire file-copy workflow.


Important Caveats

Image Volumes Are Always Read-Only

If your application needs writable storage, use:

  • emptyDir
  • PVCs
  • ephemeral storage

instead.


subPath Is Extremely Useful

If your files live under:

/site
Enter fullscreen mode Exit fullscreen mode

inside the image but you want them mounted directly into:

/usr/share/nginx/html
Enter fullscreen mode Exit fullscreen mode

then:

subPath: site
Enter fullscreen mode Exit fullscreen mode

solves that cleanly.


pullPolicy Works Exactly Like Container Images

You can use:

pullPolicy: Always
Enter fullscreen mode Exit fullscreen mode

or:

pullPolicy: IfNotPresent
Enter fullscreen mode Exit fullscreen mode

exactly as you already do with containers.


No Environment Variable Substitution

This does NOT work:

reference: ${ASSET_VERSION}
Enter fullscreen mode Exit fullscreen mode

The field is literal.

Use:

  • Helm
  • Kustomize
  • templating

instead.


Running Pods Don't Automatically Update

If somebody pushes a new image to:

:v42
Enter fullscreen mode Exit fullscreen mode

existing Pods continue using the old mounted bytes.

You must roll the Pod to pick up changes.

Which is good for reproducibility.

In production, pin image digests.


Runtime Support Matters

You need modern runtimes.

Roughly:

  • containerd >= 2.1
  • CRI-O >= 1.31

Older runtimes will fail with a clear unsupported feature error.


How imagePullSecrets Work

This is one of the nicest parts.

Image volumes automatically use the same authentication flow as normal container images.

That means Kubernetes automatically uses:

  • Pod imagePullSecrets
  • ServiceAccount imagePullSecrets
  • kubelet credential providers

No additional auth wiring required.

So this:

imagePullSecrets:
  - name: regcred
Enter fullscreen mode Exit fullscreen mode

works for BOTH:

  • container images
  • image volumes

Multiple Private Registries

If assets and application images live in different registries:

imagePullSecrets:
  - name: app-registry-creds
  - name: assets-registry-creds
Enter fullscreen mode Exit fullscreen mode

The runtime tries the secrets in order and uses whichever matches the registry hostname.


Quick Comparison

Aspect Init Container + emptyDir Image Volume
Pod complexity Multiple containers and mounts Single volume
Assets image Needs shell/cp FROM scratch works
Disk usage Image + copied bytes Image only
Startup time Pull + copy Pull only
Writable Yes No
Sharing across Pods No Yes
imagePullSecrets Pod spec Pod spec
Update without restart No No
Kubernetes support Always 1.31 alpha → 1.33 beta → 1.36 GA

When You Should Actually Use It

Image volumes are ideal when you have:

  • large read-only assets
  • independently versioned bundles
  • OCI-distributed artifacts
  • data shared across multiple Pods

Examples include:

  • ML models
  • static websites
  • OPA bundles
  • plugins
  • WASM modules
  • Grafana dashboards
  • language packs

They're especially useful when:

  • the artifacts are too large for ConfigMaps
  • you want registry-native distribution
  • you want image signing/scanning/RBAC
  • the content must remain immutable

When NOT To Use It

Don't use image volumes when:

  • the application needs writable storage
  • the content is tiny text configuration
  • the data is stateful per Pod

In those cases:

  • ConfigMaps
  • PVCs
  • emptyDir

are still better fits.


Final Thoughts

The image volume type feels small on paper, but it removes one of the longest-standing operational hacks in Kubernetes.

For years, platform engineers built elaborate init-container copy workflows just to move immutable files into Pods.

Now Kubernetes finally has a native primitive for it.

If your workloads distribute:

  • large read-only assets
  • ML models
  • frontend bundles
  • policy packs
  • plugins
  • shared runtime data

this feature can significantly reduce:

  • startup latency
  • storage duplication
  • image complexity
  • YAML noise

More importantly, it aligns Kubernetes with a broader industry shift:

OCI images are no longer just executable containers.
They're becoming the standard distribution format for software artifacts in general.

And image volumes push Kubernetes one step further in that direction.

Top comments (0)