<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Minikube on vishctl</title><link>https://vishctl.dev/tags/minikube/</link><description>Recent content in Minikube on vishctl</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 08 Sep 2026 13:30:00 +0800</lastBuildDate><atom:link href="https://vishctl.dev/tags/minikube/index.xml" rel="self" type="application/rss+xml"/><item><title>Run vLLM on Kubernetes with Minikube, WSL2 and NVIDIA GPU</title><link>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</link><pubDate>Tue, 08 Sep 2026 13:30:00 +0800</pubDate><guid>https://vishctl.dev/posts/vllm-on-wsl2-minikube/</guid><category>ai</category><category>local-llm</category><category>vllm</category><category>kubernetes</category><category>minikube</category><category>wsl2</category><category>gpu</category><category>nvidia</category><description>A practical guide to running vLLM on Kubernetes in WSL2 with Minikube, configuring NVIDIA GPU passthrough, and serving an OpenAI-compatible API.</description><content:encoded><![CDATA[<p>This is the <a href="https://docs.vllm.ai/">vLLM</a> entry in my local AI series. After testing <a href="https://vishctl.dev/posts/running-ollama-on-32gb-macbook-air/">Ollama</a>, <a href="https://vishctl.dev/posts/running-llama-cpp-on-32gb-macbook-air/">llama.cpp</a>, and <a href="https://vishctl.dev/posts/running-freetoken-on-8gb-laptop-gpu/">FreeToken</a>, I wanted to run vLLM as a Kubernetes Deployment on my Windows/WSL2 setup.</p>
<p>The plan was simple: deploy vLLM, request <code>nvidia.com/gpu: 1</code>, expose an OpenAI-compatible API endpoint through a Service, and tie it into the Kubernetes workflows I write about regularly.</p>
<p>Getting a GPU into Kubernetes on WSL2 turned into an investigation. Not because vLLM is hard, but because container runtimes and nested clusters handle GPU passthrough in non-obvious ways. Here is what happened, how the device plugin and node prerequisites work, and how to get a working GPU cluster running with Minikube.</p>
<hr>
<h2 id="what-youll-build">What You&rsquo;ll Build</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Windows
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">WSL2 Ubuntu
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Docker Engine + NVIDIA Container Toolkit
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Minikube
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ▼
</span></span><span class="line"><span class="cl">Kubernetes
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA GPU Operator
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   ├── NVIDIA Device Plugin
</span></span><span class="line"><span class="cl">   │
</span></span><span class="line"><span class="cl">   └── vLLM
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">      NVIDIA GPU
</span></span></code></pre></div><p>By the end of this guide, you&rsquo;ll have vLLM running on Kubernetes with GPU acceleration and serving an OpenAI-compatible API.</p>
<hr>
<h2 id="the-test-rig">The Test Rig</h2>
<p>My test machine for this run:</p>
<ul>
<li><strong>Host Machine:</strong> Windows 11 Laptop</li>
<li><strong>GPU:</strong> NVIDIA GeForce RTX 4070 Laptop GPU (8.0 GiB VRAM)</li>
<li><strong>Host Memory:</strong> 32 GB DDR5</li>
<li><strong>WSL2 Environment:</strong> Ubuntu 24.04 LTS (noble) with systemd enabled</li>
<li><strong>NVIDIA Drivers:</strong> Driver version 616.56 / CUDA 13.4 user-mode driver</li>
<li><strong>Kubernetes:</strong> Minikube v1.39.0 provisioning Kubernetes v1.37.0</li>
<li><strong>Model:</strong> Qwen3.5-0.8B (<code>Qwen/Qwen3.5-0.8B</code>) served by vLLM v0.28.0</li>
</ul>
<hr>
<h2 id="troubleshooting-vllm-gpu-support-on-docker-desktop-wsl2-kubernetes">Troubleshooting vLLM GPU Support on Docker Desktop WSL2 Kubernetes</h2>
<p>Docker Desktop offers a one-click Kubernetes cluster in its settings. Turning it on provisions a single-node cluster named <code>desktop-control-plane</code> running on <code>containerd://2.3.4</code>.</p>
<p>On the WSL2 host, <code>nvidia-smi</code> works fine, and running standalone GPU containers (<code>docker run --gpus all</code>) works without issues. The next step was applying the standard NVIDIA Kubernetes device plugin DaemonSet to enable GPU scheduling:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.16.2/deployments/static/nvidia-device-plugin.yml
</span></span></code></pre></div><p>The DaemonSet pod started, but immediately stalled in a retry loop:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">E... Incompatible strategy detected auto
</span></span><span class="line"><span class="cl">E... If this is a GPU node, did you configure the NVIDIA Container Toolkit?
</span></span><span class="line"><span class="cl">I... No devices found. Waiting indefinitely.
</span></span></code></pre></div><p>Checking the node capacity confirmed the issue:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node desktop-control-plane <span class="p">|</span> grep -A5 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/01-docker-desktop-k8s-no-gpu.png"
         alt="Docker Desktop Kubernetes node missing GPU resources"/> <figcaption>
            <p>Checking Docker Desktop&rsquo;s desktop-control-plane: nvidia-smi finds the RTX 4070, but the node exposes zero GPU capacity.</p>
        </figcaption>
</figure>

<p>In this tested Docker Desktop Kubernetes setup, GPU resources were not exposed to the Kubernetes node. Despite GPU access working in WSL2 and standalone Docker containers, the Kubernetes node could not expose <code>nvidia.com/gpu</code> to the device plugin.</p>
<hr>
<h2 id="how-kubernetes-detects-nvidia-gpus-using-the-nvidia-device-plugin">How Kubernetes Detects NVIDIA GPUs Using the NVIDIA Device Plugin</h2>
<p>The <a href="https://github.com/NVIDIA/k8s-device-plugin">NVIDIA Kubernetes Device Plugin</a> is a DaemonSet that exposes GPU hardware to the Kubernetes control plane. It does not run inference, and it does not install drivers or container runtimes.</p>
<p>Under standard <a href="https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/">Kubernetes GPU scheduling</a>, pods request GPU resources by specifying limits for extended resources. The device plugin integrates with Kubelet through the Kubernetes Device Plugin API over gRPC, using a Unix domain socket at <code>/var/lib/kubelet/device-plugins/kubelet.sock</code>. Its responsibility comes down to three functions:</p>
<ol>
<li><strong>Discovery:</strong> The plugin queries the host system using NVML (NVIDIA Management Library) to check how many physical GPUs are present and verify their health status.</li>
<li><strong>Registration:</strong> It registers with Kubelet and advertises the discovered GPUs as an extended allocatable resource named <code>nvidia.com/gpu</code>.</li>
<li><strong>Allocation:</strong> When a pod requesting <code>nvidia.com/gpu: 1</code> gets scheduled to the node, Kubelet calls the plugin&rsquo;s <code>Allocate</code> gRPC endpoint. The plugin picks a healthy GPU and sends back the device IDs and environment variables (<code>NVIDIA_VISIBLE_DEVICES=&lt;UUID&gt;</code>) to Kubelet. Kubelet then passes those variables to the container runtime so the container gets access to <code>/dev/nvidia*</code>.</li>
</ol>
<h3 id="worker-node-prerequisites">Worker Node Prerequisites</h3>
<p>Because the device plugin only handles discovery and Kubelet registration, it assumes the worker node already has a functional GPU stack. For worker nodes in any Kubernetes cluster, three layers must be in place before the device plugin can run:</p>
<ol>
<li><strong>Host Kernel Drivers:</strong> The node OS must have the NVIDIA kernel modules loaded (<code>nvidia.ko</code>, <code>nvidia-uvm.ko</code>) and device nodes created in <code>/dev</code>. Running <code>nvidia-smi</code> on the host must return clean output.</li>
<li><strong>NVIDIA Container Toolkit:</strong> Packages such as <code>libnvidia-container</code> and <code>nvidia-container-toolkit</code> must be installed on the host. Refer to the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">NVIDIA Container Toolkit install guide</a> for distribution setup. This toolkit includes the OCI prestart hook that inspects container environment variables and mounts host GPU driver libraries into the target container.</li>
<li><strong>Runtime Configuration:</strong> The node container runtime (containerd or Docker) must have the NVIDIA runtime configured in its daemon settings (such as <code>/etc/containerd/config.toml</code> or <code>/etc/docker/daemon.json</code>) and set up to handle CDI (Container Device Interface) or NVIDIA runtime hooks.</li>
</ol>
<p>If any of those three pieces is missing or unconfigured, the device plugin pod will fail during startup or report zero allocatable GPUs.</p>
<hr>
<h2 id="why-docker-desktop-kubernetes-cannot-detect-my-nvidia-gpu">Why Docker Desktop Kubernetes Cannot Detect My NVIDIA GPU</h2>
<p>Based on the runtime architecture observed in this setup, the issue comes down to how Docker Desktop isolates its cluster node.</p>
<p><code>desktop-control-plane</code> runs inside Docker Desktop&rsquo;s private utility VM (<code>docker-desktop</code>), isolated using <strong><code>sysbox-runc</code></strong> rather than the standard OCI runtime <code>runc</code>. Sysbox provides nested container virtualization to spin up systemd, kubelet, and containerd within an unprivileged container environment.</p>
<p>In this setup, that isolation layer prevents GPU passthrough:</p>
<ul>
<li><strong>Missing OCI Passthrough:</strong> The node container was not launched with NVIDIA GPU passthrough flags (<code>--gpus all</code>). Inside the sandbox, containerd cannot access NVIDIA device nodes (<code>/dev/nvidia*</code>) or the WSL2 DirectX driver mapping (<code>/usr/lib/wsl/lib</code>).</li>
<li><strong>Locked-Down Lifecycle:</strong> You cannot configure the NVIDIA Container Toolkit inside the node&rsquo;s containerd because Docker Desktop internally supervises the container.</li>
<li><strong>Daemon Defaults Do Not Propagate:</strong> Setting <code>nvidia</code> as the default OCI runtime in <code>docker-desktop</code> WSL does not alter the <code>sysbox-runc</code> runtime used by the Kubernetes node container.</li>
</ul>
<p>Docker has an open roadmap issue requesting native GPU passthrough for Docker Desktop Kubernetes. It remains an architectural boundary in the current setup.</p>
<p>Because of these issues, I pivoted to running Minikube with WSL2 using native Docker.</p>
<hr>
<h2 id="installing-docker-ce-and-containerd-inside-wsl2">Installing Docker CE and Containerd Inside WSL2</h2>
<p>To satisfy those node prerequisites and bypass Docker Desktop&rsquo;s VM isolation, we install the native Docker Community Edition engine directly inside the Ubuntu 24.04 WSL2 environment:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add Docker&#39;s official GPG key</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /etc/apt/keyrings
</span></span><span class="line"><span class="cl">sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">sudo chmod a+r /etc/apt/keyrings/docker.asc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set up the repository</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;deb [arch=</span><span class="k">$(</span>dpkg --print-architecture<span class="k">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable&#34;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install Docker CE and containerd</span>
</span></span><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/02-install-docker-ce-wsl2.png"
         alt="Installing Docker CE and containerd in Ubuntu WSL2"/> <figcaption>
            <p>Configuring the official Docker CE repository and installing containerd and the Docker daemon inside Ubuntu WSL2.</p>
        </figcaption>
</figure>

<p>Verify that both the Docker daemon and containerd services are running cleanly under systemd:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl status docker
</span></span><span class="line"><span class="cl">systemctl status containerd
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/03-docker-containerd-service-status.png"
         alt="Verifying systemd status for Docker and containerd"/> <figcaption>
            <p>Confirming active (running) status for both docker.service and containerd.service under WSL2 systemd.</p>
        </figcaption>
</figure>

<p>Before moving to Kubernetes, sanity check that native Docker has full GPU access through the WSL2 NVIDIA container runtime:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker container run --gpus all --rm nvidia/cuda:13.3.1-base-ubuntu26.04 nvidia-smi -L
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/04-docker-gpu-passthrough-verification.png"
         alt="Docker container GPU passthrough test"/> <figcaption>
            <p>Direct GPU passthrough test: Docker successfully identifies GPU 0 as the NVIDIA GeForce RTX 4070 Laptop GPU.</p>
        </figcaption>
</figure>

<hr>
<h2 id="run-minikube-with-nvidia-gpu-support-on-wsl2">Run Minikube with NVIDIA GPU Support on WSL2</h2>
<p>Install the latest Minikube Debian package:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
</span></span><span class="line"><span class="cl">sudo dpkg -i minikube_latest_amd64.deb
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/05-install-minikube-deb.png"
         alt="Installing Minikube package in WSL2"/> <figcaption>
            <p>Installing Minikube v1.39.0 via Debian package.</p>
        </figcaption>
</figure>

<p>Following the official <a href="https://minikube.sigs.k8s.io/docs/tutorials/nvidia/">Minikube GPU documentation</a>, starting Minikube with GPU acceleration requires specific driver and runtime parameters. Now start Minikube. Two flags matter here and both are easy to miss:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">minikube start <span class="se">\
</span></span></span><span class="line"><span class="cl">  --driver<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --container-runtime<span class="o">=</span>docker <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gpus<span class="o">=</span>all <span class="se">\
</span></span></span><span class="line"><span class="cl">  --memory<span class="o">=</span>12g <span class="se">\
</span></span></span><span class="line"><span class="cl">  --cpus<span class="o">=</span><span class="m">10</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-issuer<span class="o">=</span>https://kubernetes.default.svc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --extra-config<span class="o">=</span>apiserver.service-account-signing-key-file<span class="o">=</span>/var/lib/minikube/certs/sa.key
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/06-minikube-start-gpu-flags.png"
         alt="Starting Minikube with GPU support and Docker runtime"/> <figcaption>
            <p>Minikube starting Kubernetes v1.37.0 on Docker, automatically detecting and enabling the nvidia-device-plugin addon.</p>
        </figcaption>
</figure>

<blockquote>
<p>[!IMPORTANT]
<strong>Why <code>--container-runtime=docker</code> is required:</strong>
By default, even when using <code>--driver=docker</code>, Minikube configures containerd as its internal in-node runtime. However, Minikube&rsquo;s <code>--gpus=all</code> flag currently hooks into Docker&rsquo;s OCI runtime wrapper. If you omit <code>--container-runtime=docker</code>, Minikube fails during preflight checks with an invalid flag combination error.</p>
</blockquote>
<p>Notice the startup output: Minikube automatically detects the GPU and enables the <code>nvidia-device-plugin</code> addon for you.</p>
<hr>
<h2 id="verifying-the-cluster-and-gpu-allocation">Verifying the Cluster and GPU Allocation</h2>
<p>Install <code>kubectl</code> to talk to the cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -LO <span class="s2">&#34;https://dl.k8s.io/release/</span><span class="k">$(</span>curl -L -s https://dl.k8s.io/release/stable.txt<span class="k">)</span><span class="s2">/bin/linux/amd64/kubectl&#34;</span>
</span></span><span class="line"><span class="cl">sudo install -o root -g root -m <span class="m">0755</span> kubectl /usr/local/bin/kubectl
</span></span><span class="line"><span class="cl">kubectl version --client
</span></span><span class="line"><span class="cl">kubectl get nodes
</span></span><span class="line"><span class="cl">kubectl get pods -A
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/07-kubectl-install-and-cluster-verification.png"
         alt="Kubectl verification and nvidia-device-plugin DaemonSet"/> <figcaption>
            <p>Node minikube is Ready (v1.37.0) and the nvidia-device-plugin-daemonset is 1/1 Running in kube-system.</p>
        </figcaption>
</figure>

<p>Next, inspect the node&rsquo;s schedulable capacity:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl describe node minikube <span class="p">|</span> grep -A7 <span class="s2">&#34;Capacity:\|Allocatable:&#34;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/08-minikube-node-gpu-capacity.png"
         alt="Minikube node capacity showing GPU resource"/> <figcaption>
            <p>Cluster verification: nvidia.com/gpu: 1 is now officially registered in Capacity and Allocatable.</p>
        </figcaption>
</figure>

<p>A quick test pod confirms the GPU is reachable from inside Kubernetes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Pod
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: gpu-test
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  restartPolicy: Never
</span></span></span><span class="line"><span class="cl"><span class="s">  containers:
</span></span></span><span class="line"><span class="cl"><span class="s">    - name: cuda-test
</span></span></span><span class="line"><span class="cl"><span class="s">      image: nvidia/cuda:13.3.1-base-ubuntu26.04
</span></span></span><span class="line"><span class="cl"><span class="s">      command: [&#34;nvidia-smi&#34;, &#34;-L&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">      resources:
</span></span></span><span class="line"><span class="cl"><span class="s">        limits:
</span></span></span><span class="line"><span class="cl"><span class="s">          nvidia.com/gpu: 1
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Check the test pod logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs gpu-test
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/09-k8s-cuda-gpu-test-pod.png"
         alt="CUDA test pod running on Minikube"/> <figcaption>
            <p>Test pod running nvidia-smi inside the Kubernetes cluster and successfully accessing the RTX 4070.</p>
        </figcaption>
</figure>

<p>Real GPU scheduling and <code>nvidia.com/gpu</code> resource accounting, without Docker Desktop getting in the way.</p>
<hr>
<h2 id="deploy-vllm-on-kubernetes-with-nvidia-gpu">Deploy vLLM on Kubernetes with NVIDIA GPU</h2>
<p>Model choice for 8 GB VRAM: <strong>Qwen3.5-0.8B</strong> (<code>Qwen/Qwen3.5-0.8B</code>). Small enough to leave real headroom for vLLM&rsquo;s KV cache, which is the main reason to use vLLM over llama.cpp or Ollama in the first place, and it is a capable model at that size. As detailed in the <a href="https://docs.vllm.ai/">vLLM documentation</a>, the server exposes an OpenAI-compatible API endpoint over HTTP.</p>
<h3 id="the-shared-memory-devshm-gotcha">The Shared Memory (<code>/dev/shm</code>) Gotcha</h3>
<p>When running vLLM via the Docker CLI, passing <code>--ipc=host</code> lets workers exchange tensors and state across processes using host shared memory. In Kubernetes, pods do not share the host IPC namespace by default, and Kubernetes provisions <code>/dev/shm</code> as a minimal 64 MB tmpfs mount.</p>
<p>Some PyTorch and vLLM multiprocessing workloads can require significantly more shared memory than Kubernetes&rsquo; default <code>/dev/shm</code> (which defaults to a minimal 64 MB tmpfs mount). If insufficient shared memory is available, initialization or worker processes may fail. The Kubernetes solution is mounting an <code>emptyDir</code> volume backed by host RAM (<code>medium: Memory</code>) with a dedicated <code>sizeLimit</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># vllm-qwen-k8s.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">vllm/vllm-openai:v0.28.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--model=Qwen/Qwen3.5-0.8B&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--gpu-memory-utilization=0.7&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="s2">&#34;--max-model-len=8192&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">nvidia.com/gpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">volumeMounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/root/.cache/huggingface</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">mountPath</span><span class="p">:</span><span class="w"> </span><span class="l">/dev/shm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hf-cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dshm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">emptyDir</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="l">Memory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">sizeLimit</span><span class="p">:</span><span class="w"> </span><span class="l">2Gi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">vllm-qwen</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIP</span><span class="w">
</span></span></span></code></pre></div><p>Two practical notes on this manifest:</p>
<ul>
<li><strong>Image version pinning:</strong> I pin the image version (<code>v0.28.0</code>) here so the deployment remains reproducible. You can update the image tag after validating compatibility with your CUDA and model version.</li>
<li><strong>Hugging Face cache:</strong> <code>emptyDir</code> is used here for simplicity. The Hugging Face model cache is lost when the pod is recreated. For repeated testing or larger models, consider using a PersistentVolumeClaim (PVC) or a host-mounted path.</li>
</ul>
<p>Apply the manifest and watch the deployment roll out:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f vllm-qwen-k8s.yaml
</span></span><span class="line"><span class="cl">kubectl get pods,svc
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/10-vllm-deployment-and-service-running.png"
         alt="vLLM deployment and service running"/> <figcaption>
            <p>Applying vllm-qwen-k8s.yaml and confirming the vLLM pod reaches 1/1 Running status alongside the ClusterIP service.</p>
        </figcaption>
</figure>

<p>Stream the pod logs to inspect the vLLM initialization sequence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs vllm-qwen-5c8bbc786f-gdpmj -f
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/11-vllm-pod-startup-logs.png"
         alt="vLLM pod engine initialization logs"/> <figcaption>
            <p>vLLM v0.28.0 engine initialization: resolving Qwen3_5ForConditionalGeneration, setting max model length to 8192, and warming up CUDA graphs.</p>
        </figcaption>
</figure>

<p>Once PyTorch compilation and CUDA graph capture finish, the ASGI application completes startup and begins listening on port 8000:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/12-vllm-server-ready-and-metrics.png"
         alt="vLLM application startup complete and request logs"/> <figcaption>
            <p>vLLM application startup complete: exposing OpenAI-compatible endpoints (/v1/chat/completions) with live throughput metrics.</p>
        </figcaption>
</figure>

<hr>
<h2 id="testing-inference-and-hardware-telemetry">Testing Inference and Hardware Telemetry</h2>
<p>Forward port 8000 from the cluster service to the local machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl port-forward svc/vllm-qwen 8000:8000
</span></span></code></pre></div><p>Now issue an OpenAI-standard chat completion request using <code>curl</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://localhost:8000/v1/chat/completions <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;model&#34;: &#34;Qwen/Qwen3.5-0.8B&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;messages&#34;: [{&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Say hello in one sentence.&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">  }&#39;</span>
</span></span></code></pre></div><figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/13-vllm-curl-chat-completion-response.png"
         alt="Calling vLLM chat completions API with curl"/> <figcaption>
            <p>Inference test: 48 tokens generated cleanly through the OpenAI-compatible vLLM API running in Minikube.</p>
        </figcaption>
</figure>

<p>The response returned without issues, with server logs showing generation throughput averaging 23.5 tokens/s.</p>
<p>Finally, checking Windows Task Manager confirms the hardware utilization on the host:</p>
<figure class="post-screenshot">
    <img loading="lazy" src="https://vishctl.dev/images/posts/vllm-wsl2-minikube/14-windows-task-manager-gpu-vram-usage.png"
         alt="Windows Task Manager GPU memory usage"/> <figcaption>
            <p>Windows Task Manager telemetry: 4.4 GB of 8.0 GB dedicated VRAM allocated to the WSL2 vLLM pod at 45°C.</p>
        </figcaption>
</figure>

<p>Dedicated GPU memory held at <strong>4.4 / 8.0 GB</strong> (in line with the <code>--gpu-memory-utilization 0.7</code> setting plus desktop display overhead), with the GPU temperature at 45°C.</p>
<hr>
<h2 id="beyond-local-dev-why-production-clusters-use-the-nvidia-gpu-operator">Beyond Local Dev: Why Production Clusters Use the NVIDIA GPU Operator</h2>
<p>For a single-node laptop or a Minikube sandbox, managing drivers on the host and running a standalone device plugin DaemonSet gets the job done.</p>
<p>In production Kubernetes clusters, manual node configuration does not scale. When you operate multi-node clusters across cloud providers or bare metal, nodes get provisioned dynamically by autoscalers, Linux kernel patch levels drift, and different GPU architectures (such as A100, H100, or L40S) coexist in the same cluster.</p>
<p>This is why production setups deploy the <a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">NVIDIA GPU Operator</a> instead of managing the device plugin directly.</p>
<p>The GPU Operator automates the deployment and lifecycle management of the NVIDIA software stack required for GPU-enabled Kubernetes nodes. Driven by the <code>ClusterPolicy</code> Custom Resource Definition, it manages several components depending on your cluster configuration:</p>
<ol>
<li><strong>Driver Management:</strong> Depending on the operating environment, the Operator can compile and load the NVIDIA driver container matched to the node kernel, or use pre-installed host drivers (common in managed Kubernetes environments like GKE, EKS, or AKS).</li>
<li><strong>Container Toolkit and CDI Configuration:</strong> Depending on the Operator version and runtime configuration, GPU injection can be configured through direct NVIDIA runtime integration or CDI-based device injection (Container Device Interface), which modern releases enable by default.</li>
<li><strong>Device Plugin Supervision:</strong> It deploys and manages the lifecycle of the NVIDIA Kubernetes Device Plugin DaemonSet, keeping it in sync with driver and runtime availability.</li>
<li><strong>Node Feature Discovery (GFD):</strong> It automatically detects physical GPU capabilities and labels each node (such as <code>nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB</code> or <code>nvidia.com/gpu.family: ampere</code>). Workloads can then target specific GPU models using standard Kubernetes <code>nodeSelector</code> or affinity rules.</li>
<li><strong>Cluster Metrics with DCGM Exporter:</strong> It runs the Data Center GPU Manager (DCGM) exporter to collect metrics (GPU compute utilization, VRAM usage, temperature, power draw, and memory bandwidth) and expose them to Prometheus for cluster monitoring and alerting.</li>
<li><strong>MIG Management:</strong> On enterprise GPUs that support Multi-Instance GPU (MIG), the Operator can dynamically partition a single physical GPU into isolated hardware instances, allowing smaller workloads to share an A100 or H100 without memory interference.</li>
</ol>
<p>In short: the device plugin provides basic GPU scheduling for pods. The GPU Operator manages the operational lifecycle, monitoring, and driver stack required to run GPU infrastructure reliably in production.</p>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<ol>
<li><strong>Docker Desktop Kubernetes isolates nodes without GPU passthrough:</strong> In tested setups, <code>desktop-control-plane</code> runs in a nested <code>sysbox-runc</code> sandbox without GPU device passthrough flags.</li>
<li><strong>Minikube on native Docker CE in WSL2 works reliably:</strong> Launching Minikube with <code>--driver=docker --container-runtime=docker --gpus=all</code> automatically activates the NVIDIA device plugin addon.</li>
<li><strong>Understand device plugin vs node prerequisites:</strong> The device plugin only manages discovery and Kubelet allocation. Host drivers, Container Toolkit, and runtime configuration must already exist on the worker node.</li>
<li><strong>Always size <code>/dev/shm</code> in Kubernetes:</strong> Multiprocessing in PyTorch and vLLM quickly exceeds the default 64 MB tmpfs mount. Mount an <code>emptyDir</code> memory volume backed by host RAM.</li>
<li><strong>Budget VRAM carefully:</strong> Setting <code>--gpu-memory-utilization 0.7</code> on a compact model like Qwen3.5-0.8B preserves headroom for the KV cache on an 8 GB GPU.</li>
<li><strong>Deploy the NVIDIA GPU Operator in production:</strong> Production multi-node clusters rely on the GPU Operator to manage drivers, CDI device injection, node feature discovery, and DCGM monitoring.</li>
</ol>
<hr>
<p>Thanks for reading! If you run into issues setting up GPU passthrough on your machine, have questions, or have suggestions for other local AI setups to explore, leave a comment below.</p>
]]></content:encoded></item></channel></rss>