<?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: Slimane BOUHADI</title>
    <description>The latest articles on DEV Community by Slimane BOUHADI (@bouhadi_labs_18401393c835).</description>
    <link>https://dev.to/bouhadi_labs_18401393c835</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%2F3913756%2F99ed95a0-fcba-4cfa-a711-8f36f2efe5de.png</url>
      <title>DEV Community: Slimane BOUHADI</title>
      <link>https://dev.to/bouhadi_labs_18401393c835</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bouhadi_labs_18401393c835"/>
    <language>en</language>
    <item>
      <title>Building a Production-Grade MLOps Home Lab on Windows — K8s, LLM, RAG &amp; GitLab CI</title>
      <dc:creator>Slimane BOUHADI</dc:creator>
      <pubDate>Sat, 23 May 2026 18:42:59 +0000</pubDate>
      <link>https://dev.to/bouhadi_labs_18401393c835/building-a-production-grade-mlops-home-lab-on-windows-k8s-llm-rag-gitlab-ci-54f0</link>
      <guid>https://dev.to/bouhadi_labs_18401393c835/building-a-production-grade-mlops-home-lab-on-windows-k8s-llm-rag-gitlab-ci-54f0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I set up a complete MLOps stack on my Windows 11 PC using Multipass + k3s. This is the real guide — including every error I hit and how I fixed it. No fluff, no perfect screenshots. Just what actually worked.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Build a Home Lab?
&lt;/h2&gt;

&lt;p&gt;Cloud bills add up fast when you're learning. A local home lab gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real Kubernetes&lt;/strong&gt; — not Minikube toy mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full MLOps stack&lt;/strong&gt; — MLflow, Minio, Airflow, Ollama, Qdrant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD with GitLab&lt;/strong&gt; — actual pipelines, not tutorials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero cost&lt;/strong&gt; — runs on hardware you already own&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe sandbox&lt;/strong&gt; — break things without consequences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't just to have services running. The goal was to &lt;strong&gt;practice the full DevOps + MLOps workflow&lt;/strong&gt; end to end: push code → pipeline triggers → Terraform provisions → services deploy → metrics appear in Grafana.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Setup
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Windows 11 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;32 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;8 cores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk&lt;/td&gt;
&lt;td&gt;500 GB SSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypervisor&lt;/td&gt;
&lt;td&gt;Hyper-V (native Windows Pro)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM Manager&lt;/td&gt;
&lt;td&gt;Multipass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;k3s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Architecture decision:&lt;/strong&gt; I kept Windows as my daily driver and ran everything inside a single Ubuntu VM via Multipass. Clean separation, easy to pause/resume, no dual boot headaches.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows 11 (daily driver)
│
├── 🌐 GitLab.com (SaaS — free tier)
│    └── Pipelines + Container Registry
│
└── Multipass → vm-k3s (10 GB RAM / 4 CPU / 80 GB)
     │
     ├── ☸️  k3s (Kubernetes)
     │
     ├── ⚙️  MLOps
     │    ├── MLflow      — experiment tracking
     │    ├── Minio       — S3-compatible artifact storage
     │    └── Airflow     — pipeline orchestration
     │
     ├── 🤖 LLM Stack
     │    ├── Ollama      — run LLMs locally (CPU)
     │    └── LiteLLM    — unified OpenAI-compatible API
     │
     ├── 🔍 RAG Stack
     │    ├── Qdrant      — vector database
     │    └── LangChain   — RAG orchestration
     │
     ├── 📊 Observability
     │    ├── Prometheus  — metrics
     │    ├── Grafana     — dashboards
     │    └── Loki        — centralized logs
     │
     └── 🔐 HashiCorp Vault — secrets management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1 — Enable Hyper-V and Install Tools
&lt;/h2&gt;

&lt;p&gt;First, enable Hyper-V on Windows Pro (required for Multipass):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run as Administrator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Enable-WindowsOptionalFeature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Online&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FeatureName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft-Hyper-V-All&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Reboot when prompted&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reboot, install all tools via winget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Canonical.Multipass&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Git.Git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.VisualStudioCode&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Hashicorp.Terraform&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Helm.Helm&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Kubernetes.kubectl&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why winget over Chocolatey?&lt;/strong&gt; I originally used &lt;code&gt;choco install multipass&lt;/code&gt; and hit this error:&lt;br&gt;
&lt;code&gt;Exception calling "Start": "The specified executable is not a valid application for this OS platform."&lt;/code&gt;&lt;br&gt;
Winget installs the proper signed installer directly. Use winget.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Create the VM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;multipass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;launch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vm-k3s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10G&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--disk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;80G&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;22.04&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RAM tip:&lt;/strong&gt; I originally tried 16 GB and got:&lt;br&gt;
&lt;code&gt;Failed to allocate 16384 MB of RAM: Insufficient system resources&lt;/code&gt;&lt;br&gt;
Windows was already consuming ~20 GB. 10 GB for the VM is the sweet spot on a 32 GB machine — leaves your OS comfortable and gives k3s plenty of room.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Check your actual free RAM before creating the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_OperatingSystem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FreePhysicalMemory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TotalVisibleMemorySize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;multipass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vm-k3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Prompt becomes: ubuntu@vm-k3s:~$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3 — Install Docker + k3s
&lt;/h2&gt;

&lt;p&gt;Inside the VM:&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="c"&gt;# Docker&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ubuntu
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker

&lt;span class="c"&gt;# k3s — lightweight Kubernetes&lt;/span&gt;
curl &lt;span class="nt"&gt;-sfL&lt;/span&gt; https://get.k3s.io | sh &lt;span class="nt"&gt;-s&lt;/span&gt; - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--write-kubeconfig-mode&lt;/span&gt; 644 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable&lt;/span&gt; traefik &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;20
&lt;span class="nb"&gt;sudo &lt;/span&gt;k3s kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure kubectl:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.kube
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/rancher/k3s/k3s.yaml ~/.kube/config
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;ubuntu:ubuntu ~/.kube/config
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export KUBECONFIG=~/.kube/config'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc

kubectl get nodes
&lt;span class="c"&gt;# NAME      STATUS   ROLES                  AGE   VERSION&lt;/span&gt;
&lt;span class="c"&gt;# vm-k3s    Ready    control-plane,master   30s   v1.29.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4 — Helm + Namespaces
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Helm&lt;/span&gt;
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

&lt;span class="c"&gt;# Add repos&lt;/span&gt;
helm repo add grafana         https://grafana.github.io/helm-charts
helm repo add prometheus      https://prometheus-community.github.io/helm-charts
helm repo add open-telemetry  https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo add hashicorp       https://helm.releases.hashicorp.com
helm repo update

&lt;span class="c"&gt;# Create namespaces&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;ns &lt;span class="k"&gt;in &lt;/span&gt;mlops llm rag monitoring logging vault&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;kubectl create namespace &lt;span class="nv"&gt;$ns&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — Connect GitLab CI
&lt;/h2&gt;

&lt;p&gt;I used &lt;strong&gt;GitLab.com SaaS&lt;/strong&gt; instead of self-hosting GitLab. This saved 6 GB of RAM — GitLab CE alone needs 6+ GB. Free tier is more than enough for a home lab.&lt;/p&gt;

&lt;p&gt;Create a project on gitlab.com, grab the registration token from &lt;strong&gt;Settings → CI/CD → Runners&lt;/strong&gt;, then:&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="c"&gt;# Install GitLab Runner&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gitlab-runner
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker gitlab-runner

&lt;span class="c"&gt;# Register&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-runner register &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://gitlab.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registration-token&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_TOKEN_HERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--executor&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-image&lt;/span&gt; &lt;span class="s2"&gt;"alpine:latest"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-volumes&lt;/span&gt; &lt;span class="s2"&gt;"/var/run/docker.sock:/var/run/docker.sock"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker-privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"homelab-runner"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-list&lt;/span&gt; &lt;span class="s2"&gt;"homelab,k8s,mlops,terraform"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-untagged&lt;/span&gt; &lt;span class="nb"&gt;true

sudo &lt;/span&gt;gitlab-runner start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your runner appears green in GitLab within seconds. Every push now triggers real CI/CD on your local machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Deploy Minio (The Right Way)
&lt;/h2&gt;

&lt;p&gt;This is where I hit my first major blocker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I tried first:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;minio bitnami/minio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; mlops &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; auth.rootUser&lt;span class="o"&gt;=&lt;/span&gt;minioadmin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; auth.rootPassword&lt;span class="o"&gt;=&lt;/span&gt;minioadmin123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Failed to pull image "docker.io/bitnami/minio:2025.7.23-debian-12-r3": not found
Error: ErrImagePull → ImagePullBackOff
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bitnami generates Helm chart tags that reference Docker images which don't yet exist on Docker Hub. Classic timing issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix — use the official Minio image directly:&lt;/strong&gt;&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
  namespace: mlops
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: quay.io/minio/minio:latest
        command: ["minio", "server", "/data", "--console-address", ":9001"]
        env:
        - name: MINIO_ROOT_USER
          value: "minioadmin"
        - name: MINIO_ROOT_PASSWORD
          value: "minioadmin123"
        ports:
        - containerPort: 9000
          name: api
        - containerPort: 9001
          name: console
---
apiVersion: v1
kind: Service
metadata:
  name: minio
  namespace: mlops
spec:
  type: NodePort
  ports:
  - name: api
    port: 9000
    nodePort: 30900
  - name: console
    port: 9001
    nodePort: 30901
  selector:
    app: minio
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;quay.io/minio/minio&lt;/code&gt; is Minio's own registry — always up to date, no tag mismatch issues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 — Deploy MLflow
&lt;/h2&gt;

&lt;p&gt;MLflow needs Minio as its artifact backend. I used SQLite to keep it simple (no PostgreSQL dependency for a home lab):&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mlflow
  namespace: mlops
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mlflow
  template:
    metadata:
      labels:
        app: mlflow
    spec:
      initContainers:
      - name: create-minio-bucket
        image: quay.io/minio/mc:latest
        command: ["/bin/sh", "-c"]
        args:
          - |
            mc alias set minio http://minio:9000 minioadmin minioadmin123
            mc mb minio/mlflow --ignore-existing
      containers:
      - name: mlflow
        image: ghcr.io/mlflow/mlflow:latest
        command:
          - mlflow
          - server
          - --host=0.0.0.0
          - --port=5000
          - --backend-store-uri=sqlite:///mlflow.db
          - --default-artifact-root=s3://mlflow/
          - --serve-artifacts
        env:
        - name: MLFLOW_S3_ENDPOINT_URL
          value: "http://minio:9000"
        - name: AWS_ACCESS_KEY_ID
          value: "minioadmin"
        - name: AWS_SECRET_ACCESS_KEY
          value: "minioadmin123"
        - name: AWS_DEFAULT_REGION
          value: "us-east-1"
        ports:
        - containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
  name: mlflow
  namespace: mlops
spec:
  type: NodePort
  ports:
  - port: 5000
    nodePort: 30500
  selector:
    app: mlflow
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;initContainer&lt;/code&gt; automatically creates the &lt;code&gt;mlflow&lt;/code&gt; bucket in Minio before the server starts — no manual setup needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8 — Run a Local LLM with Ollama
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Running a real LLM on your local machine, inside Kubernetes, on CPU only.&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;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: llm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
      - name: ollama
        image: ollama/ollama:latest
        ports:
        - containerPort: 11434
        env:
        - name: OLLAMA_NUM_PARALLEL
          value: "1"
        - name: OLLAMA_MAX_LOADED_MODELS
          value: "1"
        resources:
          requests:
            memory: "2Gi"
            cpu: "1"
          limits:
            memory: "4Gi"
            cpu: "3"
        volumeMounts:
        - name: ollama-data
          mountPath: /root/.ollama
      volumes:
      - name: ollama-data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: ollama
  namespace: llm
spec:
  type: NodePort
  ports:
  - port: 11434
    nodePort: 31434
  selector:
    app: ollama
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Critical: always set resource limits on shared VMs.&lt;/strong&gt; Without limits, Ollama will consume all available RAM and OOMKill your other pods. I learned this the hard way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Choosing the Right Model for CPU
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;RAM needed&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mistral 7B Q4&lt;/td&gt;
&lt;td&gt;4.3 GB&lt;/td&gt;
&lt;td&gt;Too heavy for 4 GB limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-3 Mini&lt;/td&gt;
&lt;td&gt;3.5 GB&lt;/td&gt;
&lt;td&gt;Still too heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;llama3.2:1b&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Perfect for CPU home lab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemma2:2b&lt;/td&gt;
&lt;td&gt;1.6 GB&lt;/td&gt;
&lt;td&gt;✅ Good alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; llm &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Pull the model&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; llm &lt;span class="nv"&gt;$OLLAMA_POD&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; ollama pull llama3.2:1b

&lt;span class="c"&gt;# Test it&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; llm &lt;span class="nv"&gt;$OLLAMA_POD&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; ollama run llama3.2:1b &lt;span class="s2"&gt;"Explain RAG in 2 sentences"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test via API:&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="nv"&gt;VM_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;:31434/api/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "llama3.2:1b",
    "prompt": "What is MLOps?",
    "stream": false
  }'&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys,json; print(json.load(sys.stdin)['response'])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 9 — Observability Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Prometheus + Grafana&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;kube-prometheus prometheus/kube-prometheus-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; grafana.service.type&lt;span class="o"&gt;=&lt;/span&gt;NodePort &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; grafana.service.nodePort&lt;span class="o"&gt;=&lt;/span&gt;30300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; grafana.adminPassword&lt;span class="o"&gt;=&lt;/span&gt;admin123 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# Loki (log aggregation) + Promtail (log shipper)&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;loki grafana/loki-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; logging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; grafana.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; promtail.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access Grafana:&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="nv"&gt;VM_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Grafana: http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30300"&lt;/span&gt;
&lt;span class="c"&gt;# Login: admin / admin123&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add Loki as a data source in Grafana:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Settings → Data Sources → Add → Loki&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;URL: &lt;code&gt;http://loki.logging:3100&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now you have unified logs + metrics in one dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 10 — Vault for Secrets Management
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;vault hashicorp/vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; server.dev.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; server.dev.devRootToken&lt;span class="o"&gt;=&lt;/span&gt;root &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ui.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ui.serviceType&lt;span class="o"&gt;=&lt;/span&gt;NodePort &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; ui.serviceNodePort&lt;span class="o"&gt;=&lt;/span&gt;30820

&lt;span class="c"&gt;# Store your first secret&lt;/span&gt;
&lt;span class="nv"&gt;VAULT_POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; vault &lt;span class="nt"&gt;-l&lt;/span&gt; app.kubernetes.io/name&lt;span class="o"&gt;=&lt;/span&gt;vault &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; vault &lt;span class="nv"&gt;$VAULT_POD&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; vault secrets &lt;span class="nb"&gt;enable &lt;/span&gt;kv-v2
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; vault &lt;span class="nv"&gt;$VAULT_POD&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; vault kv put secret/homelab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;minio_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minioadmin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;minio_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minioadmin123

&lt;span class="nv"&gt;VM_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Vault UI: http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30820  (token: root)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your GitLab CI pipeline, reference Vault secrets instead of hardcoding them in variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture — All Services Running
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VM_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Your Home Lab ==="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"MLflow   : http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30500"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Minio    : http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30901"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Grafana  : http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30300  (admin/admin123)"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Vault    : http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:30820  (token: root)"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Ollama   : http://&lt;/span&gt;&lt;span class="nv"&gt;$VM_IP&lt;/span&gt;&lt;span class="s2"&gt;:31434"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Bitnami Helm charts break on image tags&lt;/strong&gt;&lt;br&gt;
Don't use &lt;code&gt;bitnami/minio&lt;/code&gt; — it references images that don't exist yet. Use &lt;code&gt;quay.io/minio/minio:latest&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Always set Kubernetes resource limits on shared VMs&lt;/strong&gt;&lt;br&gt;
Without limits, one greedy pod (looking at you, Ollama) will OOMKill everything else. Set &lt;code&gt;limits.memory&lt;/code&gt; always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. RAM planning matters more than you think&lt;/strong&gt;&lt;br&gt;
On a 32 GB machine, Windows itself consumes ~20 GB. That leaves 12 GB for your VM. Budget carefully — 10 GB for the VM is the realistic sweet spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. GitLab SaaS &amp;gt; self-hosted for a home lab&lt;/strong&gt;&lt;br&gt;
Self-hosting GitLab CE needs 6+ GB of RAM just to idle. GitLab.com free tier gives you unlimited private repos, 400 CI/CD minutes/month, and a container registry. Use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Start small with LLMs on CPU&lt;/strong&gt;&lt;br&gt;
Forget Mistral 7B on CPU without a GPU. &lt;code&gt;llama3.2:1b&lt;/code&gt; is surprisingly capable for RAG experiments and uses only 1.3 GB. Add GPU passthrough later if you need more power.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Use &lt;code&gt;winget&lt;/code&gt; not &lt;code&gt;choco&lt;/code&gt; for Multipass on Windows&lt;/strong&gt;&lt;br&gt;
Chocolatey's Multipass package uses an installer that fails on recent Windows builds. &lt;code&gt;winget install Canonical.Multipass&lt;/code&gt; works every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This setup is a solid foundation. Here's what I'm building on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubeflow Pipelines&lt;/strong&gt; — proper ML pipeline orchestration on K8s&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry Collector&lt;/strong&gt; — unified traces/metrics/logs routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datadog integration&lt;/strong&gt; — ship everything to cloud observability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform IaC&lt;/strong&gt; — replace all &lt;code&gt;kubectl apply&lt;/code&gt; with proper infrastructure as code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG pipeline&lt;/strong&gt; — Qdrant + LangChain + Ollama end-to-end&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# VM management (Windows PowerShell)&lt;/span&gt;
multipass list                    &lt;span class="c"&gt;# list VMs&lt;/span&gt;
multipass shell vm-k3s            &lt;span class="c"&gt;# enter VM&lt;/span&gt;
multipass &lt;span class="nb"&gt;suspend &lt;/span&gt;vm-k3s          &lt;span class="c"&gt;# pause (saves RAM)&lt;/span&gt;
multipass start vm-k3s            &lt;span class="c"&gt;# resume&lt;/span&gt;

&lt;span class="c"&gt;# Inside the VM&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;               &lt;span class="c"&gt;# all pods&lt;/span&gt;
kubectl top pods &lt;span class="nt"&gt;-A&lt;/span&gt;               &lt;span class="c"&gt;# RAM/CPU usage&lt;/span&gt;
free &lt;span class="nt"&gt;-mh&lt;/span&gt;                          &lt;span class="c"&gt;# available RAM&lt;/span&gt;
watch kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;         &lt;span class="c"&gt;# live monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://k3s.io" rel="noopener noreferrer"&gt;k3s Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mlflow.org" rel="noopener noreferrer"&gt;MLflow Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library" rel="noopener noreferrer"&gt;Ollama Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://multipass.run" rel="noopener noreferrer"&gt;Multipass&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/vault" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built this through caffeine and &lt;code&gt;kubectl describe pod&lt;/code&gt; debugging. If you hit issues I didn't cover, drop a comment — happy to help.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this saved you time, leave a ❤️ — it helps others find it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>llm</category>
      <category>k8s</category>
      <category>mlops</category>
    </item>
  </channel>
</rss>
