{"id":486,"date":"2025-09-10T11:28:34","date_gmt":"2025-09-10T11:28:34","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=486"},"modified":"2025-09-10T11:28:34","modified_gmt":"2025-09-10T11:28:34","slug":"oci-image-volume-kubernetes-pods","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=486","title":{"rendered":"How to Mount OCI Image as Volume in Kubernetes Pod"},"content":{"rendered":"<p>In this guide, you will learn how to mount OCI container images as volumes in Kubernetes Pods using the ImageVolume feature.<\/p>\n<p>This feature is especially useful for ML projects that work with LLMs, as it allows you to package model data in OCI images and easily switch between different models.<\/p>\n<h2 id=\"imagevolume-beta-feature\">ImageVolume (beta feature)<\/h2>\n<p>Kubernetes version 1.31 has introduced a new alpha feature that allows you to use OCI image volumes directly within <a href=\"https:\/\/devopscube.com\/kubernetes-pod\/\" rel=\"noreferrer\">Kubernetes pods<\/a>.&nbsp;In Kubernetes version 1.33, it has been changed to a beta feature.<\/p>\n<p>So what are OCI images?<\/p>\n<p>OCI images are images that follow <a href=\"https:\/\/opencontainers.org\/?ref=devopscube.com\" rel=\"noreferrer\">Open Container Initiative<\/a> specifications. For example, <a href=\"https:\/\/devopscube.com\/what-is-docker\/\" rel=\"noreferrer\">Docker<\/a>, <a href=\"https:\/\/devopscube.com\/podman-tutorial-beginners\/\" rel=\"noreferrer\">Podman<\/a>, containerd and <strong>CRI-O<\/strong> runtimes used OCI image specifications for images.<\/p>\n<p>You can use the&nbsp;<strong><code>ImageVolume<\/code><\/strong>&nbsp;feature to store binary artifacts in images and mount them to pods. Also, a key thing about this feature is, it is a <strong>read only Volume.<\/strong><\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-52.png\" class=\"kg-image\" alt=\"Mount OCI Image as Volume in Kubernetes Pods\" loading=\"lazy\" width=\"1920\" height=\"1541\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-52.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2025\/09\/image-52.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2025\/09\/image-52.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-52.png 1920w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, lets get started with the practical example.<\/p>\n<h2 id=\"step-1-enable-the-imagevolume-feature-gate\">Step 1: Enable the ImageVolume Feature Gate<\/h2>\n<p><strong>ImageVolume<\/strong> feature is not enabled by default. <\/p>\n<p>So, to mount OCI image in a pods volume, first you need to enable the <a href=\"https:\/\/devopscube.com\/enable-feature-gates-kubeadm\/\" rel=\"noreferrer\">feature gate <\/a><strong><code>ImageVolume<\/code><\/strong> in the <a href=\"https:\/\/devopscube.com\/setup-kubernetes-cluster-kubeadm\/\" rel=\"noreferrer\">Kubernetes cluster<\/a><\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\u26a0\ufe0f<\/div>\n<div class=\"kg-callout-text\">If you are using CRI-O as the runtime, the version should be above or <b><strong style=\"white-space: pre-wrap;\">atleast v1.31<\/strong><\/b>, and if the runtime you are using containerd, the version should be above or <b><strong style=\"white-space: pre-wrap;\">atleast v2.1.0.<\/strong><\/b><\/div>\n<\/div>\n<p>To enable this feature gate, you have to modify the API server manifest and kubelet config file as given below.<\/p>\n<p>To edit the API server manifest file, open the manifest file using the following command.<\/p>\n<pre><code class=\"language-bash\">sudo vi \/etc\/kubernetes\/manifests\/kube-apiserver.yaml\n<\/code><\/pre>\n<p>Then add the following line.<\/p>\n<pre><code class=\"language-bash\">- --feature-gates=ImageVolume=true<\/code><\/pre>\n<p>Once you save the file, the API server will restart automatically.<\/p>\n<p>Now, modify the kubelet config file.<\/p>\n<pre><code class=\"language-bash\">sudo vi \/var\/lib\/kubelet\/config.yaml<\/code><\/pre>\n<p>And add the following block inside the config file.<\/p>\n<pre><code class=\"language-bash\">featureGates:\n  ImageVolume: true<\/code><\/pre>\n<p>Then restart the kubelet.<\/p>\n<pre><code class=\"language-bash\">sudo systemctl daemon-reload\n\nsudo systemctl restart kubelet<\/code><\/pre>\n<h2 id=\"testing-in-kind-cluster-optional\">Testing in Kind Cluster (Optional)<\/h2>\n<p>If you just want to check this feature and you dont have an active cluster, you can create a Kind cluster in which we can enable ImageVolume feature gates during creation.<\/p>\n<pre><code class=\"language-yaml\">kind: Cluster\napiVersion: kind.x-k8s.io\/v1alpha4\nname: multi-node-cluster\nfeatureGates:\n  ImageVolume: true\nnodes:\n  - role: control-plane\n    image: kindest\/node:v1.33.1\n    extraPortMappings:\n      - containerPort: 30000\n        hostPort: 30000\n        protocol: TCP\n      - containerPort: 31000\n        hostPort: 31000\n        protocol: TCP\n      - containerPort: 32000\n        hostPort: 32000\n        protocol: TCP\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\n    image: kindest\/node:v1.33.1\n  - role: worker\n    image: kindest\/node:v1.33.1<\/code><\/pre>\n<h2 id=\"step-2-build-an-oci-image\">Step 2: Build an OCI Image<\/h2>\n<p>The next step is to build an OCI image.<\/p>\n<p>For this example, I am using a prediction model that I have locally. You can replace the model file with any file for testing.<\/p>\n<p>Here is the Dockerfile.<\/p>\n<pre><code>FROM scratch\nCOPY model.pkl \/models\/model.pkl<\/code><\/pre>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-text\">I have uploaded this image to Docker Hub as&nbsp;<code spellcheck=\"false\" style=\"white-space: pre-wrap;\">devopscube\/oci-image:1.0<\/code>.<br \/>You can use it directly for testing.<\/div>\n<\/div>\n<h2 id=\"step-3-deploy-a-predictor-application\">Step 3: Deploy a Predictor Application<\/h2>\n<p>To test the ImageVolume, we will deploy a simple Python predictor application. This application loads the <code>model.pkl<\/code> file directly from the mounted image volume.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-text\">I have already built the app and published it as&nbsp;<code spellcheck=\"false\" style=\"white-space: pre-wrap;\">devopscube\/predictor:1.0<\/code>, which you can use for testing.<\/div>\n<\/div>\n<p>Here is the python code that is part of the predictor image.<\/p>\n<pre><code class=\"language-python\">import os\nimport joblib\nimport numpy as np\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nMODEL_PATH = os.environ.get(\"MODEL_PATH\", \"\/models\/model.pkl\")\nmodel = joblib.load(MODEL_PATH)\n\napp = FastAPI()\n\nclass PredictIn(BaseModel):\n    instances: list\n\n@app.get(\"\/healthz\")\ndef healthz():\n    return {\"ok\": True, \"model_path\": MODEL_PATH}\n\n@app.post(\"\/v1\/models\/model:predict\")\ndef predict(p: PredictIn):\n    X = p.instances\n    try:\n        X_arr = np.array(X, dtype=float)\n        preds = model.predict(X_arr).tolist()\n    except Exception:\n        preds = model.predict(X).tolist()\n    return {\"predictions\": preds}<\/code><\/pre>\n<h2 id=\"step-4-test-the-imagevolume\">Step 4: Test the ImageVolume<\/h2>\n<p>Now, lets deploy the predictor image and the OCI volume image to test the image volume.<\/p>\n<p>Here are the Deployment and Service manifests.<\/p>\n<pre><code class=\"language-yaml\">apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: predictor\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: predictor\n  template:\n    metadata:\n      labels:\n        app: predictor\n    spec:\n      containers:\n      - name: app\n        image: devopscube\/predictor:1.0                    \n        ports:\n        - containerPort: 8000\n        env:\n        - name: MODEL_PATH\n          value: \/volume\/models\/model.pkl\n        volumeMounts:\n        - name: model-vol\n          mountPath: \/volume\n      volumes:\n      - name: model-vol\n        image:\n          reference: devopscube\/oci-image:1.0\n          pullPolicy: IfNotPresent\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: predictor-svc\nspec:\n  selector:\n    app: predictor\n  ports:\n  - port: 80\n    targetPort: 8000<\/code><\/pre>\n<p>Deploy the above manifest and once the pod is running without issues,&nbsp;check if the&nbsp;<code>model.pkl<\/code>&nbsp;file is inside the volume.<\/p>\n<pre><code class=\"language-bash\">$ kubectl exec -it predictor-7f7ff66689-gwg5w -- ls \/volume\/models\n\nmodel.pkl<\/code><\/pre>\n<p>Now run the following command to port-forward the predictor service so that we can test the prediction endpoint.<\/p>\n<pre><code class=\"language-bash\">kubectl port-forward svc\/predictor-svc 8080:80<\/code><\/pre>\n<p>Now, use the following&nbsp;<code>curl<\/code>&nbsp;command from your workstation to send a prediction request to the predictor application. This will validate whether the application is able to access the&nbsp;<code>model.pkl<\/code>&nbsp;file from the&nbsp;<strong><code>ImageVolume<\/code><\/strong>.<\/p>\n<pre><code class=\"language-bash\">curl -X POST \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\n        \"instances\": [\n          \"sparrow\",\n          \"elephant\",\n          \"rose\"     \n        ]\n      }' \\\n  \"http:\/\/127.0.0.1:8080\/v1\/models\/model:predict\"<\/code><\/pre>\n<p>You will get the following output.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/media.beehiiv.com\/cdn-cgi\/image\/fit=scale-down,format=auto,onerror=redirect,quality=80\/uploads\/asset\/file\/6984c43d-842f-456e-a738-d847f8ded191\/image.png?t=1756183770\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"960\" height=\"330\"><\/figure>\n<p>This is the expected output,&nbsp;<code>0<\/code>&nbsp;means animal,&nbsp;<code>1<\/code>&nbsp;means bird, and&nbsp;<code>2<\/code>&nbsp;means plant.<\/p>\n<p>That\u2019s a wrap! \ud83c\udf89<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>In this post, you learned how to enable the ImageVolume feature in Kubernetes, build an OCI image, and test it with a sample predictor ML application. <\/p>\n<p>This feature makes it easy to package data, tools , or models and switch between models without managing complex storage setups.<\/p>\n<p>Refer the <a href=\"https:\/\/devopscube.com\/kubernetes-ai-ml-features\/\" rel=\"noreferrer\">Kubernetes ML features<\/a> blog to know more about native ML support in Kubernetes.<\/p>\n<p>If you want to learn more Kubernetes concepts, look at our <a href=\"https:\/\/devopscube.com\/kubernetes-tutorials-beginners\/\" rel=\"noreferrer\">Kubernetes tutorial<\/a> blog.<\/p>\n<p>Try this feature and let me know how it goes!<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/oci-image-volume-kubernetes-pods\/\" target=\"_blank\" rel=\"noopener noreferrer\">How to Mount OCI Image as Volume in Kubernetes Pod \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/oci-image-volume-kubernetes-pods\/<\/p>\n","protected":false},"author":1,"featured_media":487,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-486","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/486","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=486"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/486\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/487"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=486"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=486"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=486"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}