Image Volume in Practice: os/arch Makes No Sense for Content Images
Image Volume in Practice: os/arch Makes No Sense for Content Images
Kubernetes Image Volumes let you put files into an OCI image and mount that image into a Pod as a volume — separate from the application container. The feature graduated to stable in Kubernetes 1.36.
The use case: separating runtime images from application content. For example:
- Nginx + HTML/CSS/JS
- Java + JAR
- Node.js + .js files
- Python + .py files
This sounds simple. It exposed a surprisingly annoying problem.
The goal
Take a traditional Nginx site:
FROM nginx:latest
COPY ./site /usr/share/nginx/html
The Nginx runtime and website are one image. A security update to Nginx forces a rebuild of the website, and vice versa. Neither has anything to do with the other.
Instead, define two independent images:
runtimeImage: nginx:latest
siteContentImage: xyzSite:x.y.z
Mount xyzSite as an Image Volume. Now the runtime and content have independent lifecycles.
The problem: architecture metadata
Content images don’t contain executables. They contain HTML, CSS, and JS. But OCI images require platform metadata (linux/amd64, linux/arm64), and that causes issues at build, publish, and deploy.
Build
If you don’t specify a platform, the build environment picks the default — usually the OS/arch of the build machine. That’s fine for application images. It’s meaningless for content images.
Publish
A multi-arch manifest works but duplicates content:
amd64 → HTML / CSS / JS / images
arm64 → HTML / CSS / JS / images
For a 5 MB website, that’s fine. For a 100 GB AI model, it’s not.
Deploy
A mixed-architecture cluster fails. An amd64-only content image won’t mount on an arm64 node — kubelet checks the platform metadata, even though the content is platform-independent. The result: ImagePullBackOff.
The two bad options
- Build multi-arch content images. Solves deployment, but stores
Ncopies of the same content forNarchitectures. - Pick one architecture. Works until you add a node with a different architecture.
The fix: unknown/unknown
OCI images carry platform metadata in their config:
{
"architecture": "amd64",
"os": "linux"
}
The OCI spec requires these to be valid GOOS/GOARCH values — there’s no built-in way to express a platform-neutral image.
Setting them to unknown works:
{
"architecture": "unknown",
"os": "unknown"
}
The image loads as a data volume on both amd64 and arm64 nodes. Docker rejects unknown/unknown, but Kubernetes Image Volumes accept it.
The converter
Repacking images manually is tedious. image-volume-converter is a Go utility that automates it. It reads an existing image, sets os and architecture to unknown, and writes the result. It does not rebuild the application or filesystem layers — it only changes metadata.
GitHub Actions
Use it in a pipeline:
- name: Build image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
load: true
tags: ghcr.io/${{ github.repository }}:main-latest
- name: Create no-arch content image
uses: kubehub-io/image-volume
with:
imageTag: ghcr.io/${{ github.repository }}:main-latest
outputImageTag: ghcr.io/${{ github.repository }}:main-noarch
Or export as an OCI archive:
- name: Export OCI archive
uses: kubehub-io/image-volume
with:
imageTag: ghcr.io/${{ github.repository }}:main-latest
publishTo: OCIArchive:/tmp/site-no-arch.tar
Note: SBOMs, provenance, and attestations are currently ignored by the converter.
Usage
Mount the converted image in a Pod spec:
containers:
- name: staticweb
image: nginx:alpine
volumeMounts:
- name: site-content
mountPath: /usr/share/nginx/html
subPath: site
readOnly: true
volumes:
- name: site-content
image:
reference: mycr.io/myimage:mytag
pullPolicy: IfNotPresent
No ImagePullBackOff. No duplicate content. Independent lifecycles for runtime and content.
Source: kubehub-io/image-volume