🧪 Lab — Working With Kubernetes & Minikube
Duration: ~1 Goal: Learn how to start a local Kubernetes cluster with Minikube, deploy workloads using YAML, interact with Pods/Jobs/CronJobs, and understand how storage shares data between workloads.
📘 Learning Objectives
By the end of this lab, you will be able to:
- Install kubectl and minikube (Windows / macOS / Ubuntu)
- Start a local Kubernetes cluster and verify it’s healthy
- Deploy a Pod from a YAML manifest and interact with it
- Use
kubectl port-forward,logs,exec, andcp - Run a Job and confirm it completed successfully
- Use a CronJob to refresh content on a schedule
- Use a PersistentVolumeClaim (PVC) to share data between workloads
- Use a Deployment with an initContainer to enforce startup ordering
1️⃣ Install Minikube + kubectl
You need kubectl (Kubernetes CLI) and minikube (local single-node Kubernetes).
Minikube also needs a “driver” (Docker is easiest on all OSes).
🪟 Windows 10/11
1) Install kubectl
Use the official kubectl guide for Windows (recommended method is downloading the binary or using a package manager).
Quick option (Winget):
winget install Kubernetes.kubectl
Verify:
kubectl version --client
2) Install Minikube
Option A (Winget):
winget install Kubernetes.minikube
Option B (Chocolatey):
choco install minikube
Verify:
minikube version
3) Start a local cluster (Docker driver recommended)
Install Docker Desktop first (WSL2 recommended). Then:
minikube start --driver=docker
kubectl get nodes
🍎 macOS
1) Install kubectl
If you use Homebrew:
brew install kubectl
kubectl version --client
2) Install Minikube
Using Homebrew:
brew install minikube
minikube version
3) Start a local cluster
If you have Docker installed:
minikube start --driver=docker
kubectl get nodes
🐧 Ubuntu (22.04/24.04)
1) Install kubectl
Option A (snap):
sudo snap install kubectl --classic
kubectl version --client
2) Install Minikube
Binary install (amd64):
curl -LO https://github.com/kubernetes/minikube/releases/latest/download/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube version
3) Start a local cluster
If Docker is installed:
minikube start --driver=docker
kubectl get nodes
✅ Quick Cluster Health Check (all OS)
kubectl get nodes
kubectl get pods -A
minikube status
🧩 Part A — Nginx Pod & Basic Kubectl Commands
Provide the YAML that runs a Pod with Nginx 1.27.4-alpine as well as the kubectl commands needed to:
YAML manifest: Nginx 1.27.4-alpine Pod
Create a file named nginx-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: nginx-alpine
labels:
app: nginx-alpine
spec:
containers:
- name: nginx
image: nginx:1.27.4-alpine
ports:
- containerPort: 80
1. Start the Pod
kubectl apply -f nginx-pod.yaml
kubectl get pods
kubectl describe pod nginx-alpine
Wait until it’s Running:
2. Forward port 80 locally and test in browser/curl
Forward local port 8000 → Pod port 80:
kubectl port-forward pod/nginx-alpine 8000:80
In a second terminal:
curl http://localhost:8000/
Expected answer: the default Nginx welcome page HTML (it typically contains “Welcome to nginx!”).
Stop port-forward with Ctrl+C when done.
3. See logs of the running container
kubectl logs nginx-alpine
4. Open a shell session inside the container and edit the default page
Open a shell in the container (Alpine uses sh, not bash):
kubectl exec -it nginx-alpine -- sh
Edit the first sentence in /usr/share/nginx/html/index.html:
The container is minimal (no nano). Use
sed:
cd /usr/share/nginx/html
# Replace the first occurrence of the “Welcome to nginx!” text
sed -i 's/Welcome to nginx!/Welcome to MY nginx!/g' index.html
exit
Validate:
kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/ | head
5. From your computer terminal: download page locally, upload a new one
Download the page locally:
kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/index.html -o index.html
Create a replacement page (example):
cat > index.html <<'EOF'
<!DOCTYPE html>
<html>
<body>
<h1>Replaced from my computer!</h1>
<p>Uploaded using kubectl cp.</p>
</body>
</html>
EOF
Upload it into the Pod:
kubectl cp ./index.html nginx-alpine:/usr/share/nginx/html/index.html
Validate:
kubectl port-forward pod/nginx-alpine 8000:80
curl http://localhost:8000/
6. Stop the Pod and remove it from Kubernetes
A Pod created directly is removed by deleting it (or deleting the YAML):
kubectl delete -f nginx-pod.yaml
# or:
kubectl delete pod nginx-alpine
Confirm it’s gone:
kubectl get pods
🧩 Part B: Job + ConfigMap Script + CronJob + Shared Volume + Deployment + initContainer
Note: PVC –> PersistentVolumeClaim
In this part, you’ll download a website using:
wget -E -k -p <url>
You will share the downloaded content with Nginx using a PVC.
Minikube is usually single-node, so ReadWriteOnce PVCs work well for sharing between Pods scheduled on the same node.
1. YAML: Job (Ubuntu 24.04) that downloads ceid.upatras.gr using a ConfigMap script
Create site-downloader-job.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: download-script
data:
download.sh: |
#!/usr/bin/env bash
# -e : exit immediately if a command fails
# -u : treat unset variables as an error
# -o pipefail : fail if any command in a pipeline fails
set -euo pipefail
# Install tools needed to download HTTPS content
apt-get update
apt-get install -y --no-install-recommends wget ca-certificates
rm -rf /var/lib/apt/lists/*
# /data is the PVC mount. Anything written here persists and is shared with Nginx.
mkdir -p /data
# Download the website snapshot to /data_tmp
# -p: download prerequisites (css/js/images)
# -k: convert links for offline browsing
# -E: add .html extension when needed
# -nH: do NOT create a host folder like /data_tmp/ceid.upatras.gr/
wget -E -k -p -nH https://ceid.upatras.gr/ -P /data
echo "Download finished."
---
apiVersion: batch/v1
kind: Job
metadata:
name: download-ceid-upatras
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: ubuntu
image: ubuntu:24.04
command: ["/bin/bash", "/scripts/download.sh"]
volumeMounts:
- name: script
mountPath: /scripts
volumes:
- name: script
configMap:
name: download-script
defaultMode: 0755
Apply:
kubectl apply -f site-downloader-job.yaml
kubectl get jobs
kubectl get pods
2. Which command confirms the Job completed successfully?
kubectl get job download-ceid-upatras
(You can also check logs from the Job Pod)
kubectl logs job/download-ceid-upatras
3. Download Ceid Upatras page and serve it through a container
- PVC for persistent shared storage (
site-pvc) - ConfigMap that stores a download script (
download-script) - Job to do an initial download (
download-ceid-upatras) - Nginx Pod that serves the downloaded content (
nginx-site) - CronJob that refreshes the content nightly (
refresh-ceid-upatras-nightly)
✅ Big Picture: What’s the goal?
You want Kubernetes to:
- Download the web site
https://ceid.upatras.gr/into a persistent volume. - Serve the downloaded static files through Nginx.
- Refresh (re-download) the content automatically every night at 02:15.
This is achieved by having a downloader workload (Job/CronJob) and the Nginx workload share the same persistent volume.
🔁 Data Flow (the key idea)
Shared storage path mapping
- Downloader containers mount the PVC at:
/data - Nginx mounts the same PVC at:
/usr/share/nginx/html
So the directory layout is:
- Downloader writes:
/data/index.html,/data/somefile.css, … - Nginx reads and serves:
/usr/share/nginx/html/index.html,/usr/share/nginx/html/somefile.css, …
These are the same underlying files, because both are backed by the same PVC.
🧱 Why you need a PVC
Pods and containers are ephemeral:
- If the Pod restarts or is recreated, its container filesystem returns to the image default.
A PersistentVolumeClaim (PVC) gives you:
- data that persists beyond Pod lifetime
- a shared filesystem that multiple Pods can mount (within access mode limits)
In Minikube, PVCs typically work well for this kind of local lab.
🧩 Component-by-component logic
1) PersistentVolumeClaim — site-pvc
Purpose: allocate storage that will hold the mirrored website.
ReadWriteOncemeans: the volume can be mounted read/write by Pods on a single node.- In Minikube (single node), that’s fine.
2) ConfigMap — download-script
Purpose: store the download script as data in Kubernetes.
Why a ConfigMap?
- It keeps the script versioned in YAML
- You don’t need to bake a custom Docker image just to run a script
- Any Pod can mount it and run it
The script is mounted inside Pods at /scripts/download.sh.
3) The download script logic
This is what the script does, in order:
-
Install dependencies
It installswgetandca-certificatesinside the Ubuntu container so HTTPS downloads work.apt-get update apt-get install -y --no-install-recommends wget ca-certificates -
Download the website into
/datawget -E -k -p -nH https://ceid.upatras.gr/ -P /dataFlags meaning:
-p: download all page prerequisites (images/css/js)-k: convert links for local browsing-E: add.htmlextension when appropriate-nH: do not create a host folder like/data_tmp/ceid.upatras.gr/-P: destination folder
4) Job — download-ceid-upatras
Purpose: run once (manually / on apply) to populate the PVC immediately.
This is useful because:
- CronJob runs only at 02:15.
- You want the site available right away for testing.
The Job:
- mounts the script ConfigMap
- mounts the PVC at
/data - runs
/scripts/download.sh
You can confirm completion with:
kubectl get job download-ceid-upatras
kubectl wait --for=condition=complete job/download-ceid-upatras --timeout=10m
5) Nginx Pod — nginx-site
Purpose: serve the downloaded content.
Nginx serves static files from:
/usr/share/nginx/html
You mount the PVC there:
mountPath: /usr/share/nginx/html
So Nginx serves whatever the Job/CronJob downloaded.
Because it’s readOnly: true, Nginx cannot accidentally modify the site files.
Test it with:
kubectl port-forward pod/nginx-site 8080:80
curl http://localhost:8080/
6) CronJob — refresh-ceid-upatras-nightly
Purpose: refresh the website every night automatically.
Schedule:
schedule: "15 2 * * *"
This means:
- every day
- at 02:15 (cluster time)
CronJob creates a Job each run, which:
- mounts the same PVC
- runs the same script
- updates the content served by Nginx
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: site-pvc
spec:
# In Minikube this usually maps to a local storage provisioner.
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: download-script
data:
download.sh: |
#!/usr/bin/env bash
# -e : exit immediately if a command fails
# -u : treat unset variables as an error
# -o pipefail : fail if any command in a pipeline fails
set -euo pipefail
# Install tools needed to download HTTPS content
apt-get update
apt-get install -y --no-install-recommends wget ca-certificates
rm -rf /var/lib/apt/lists/*
# /data is the PVC mount. Anything written here persists and is shared with Nginx.
mkdir -p /data
# Download the website snapshot to /data_tmp
# -p: download prerequisites (css/js/images)
# -k: convert links for offline browsing
# -E: add .html extension when needed
# -nH: do NOT create a host folder like /data_tmp/ceid.upatras.gr/
wget -E -k -p -nH https://ceid.upatras.gr/ -P /data
echo "Download finished."
---
apiVersion: batch/v1
kind: Job
metadata:
name: download-ceid-upatras
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: ubuntu
image: ubuntu:24.04
# Run the script that is stored in the ConfigMap and mounted at /scripts/download.sh
command: ["/bin/bash", "/scripts/download.sh"]
volumeMounts:
# Mount the ConfigMap containing download.sh
- name: script
mountPath: /scripts
# Mount the PVC at /data so downloaded files persist and can be shared with Nginx
- name: site
mountPath: /data
volumes:
- name: script
configMap:
name: download-script
# Make the script executable (0755 decimal is 493)
defaultMode: 0755
- name: site
persistentVolumeClaim:
claimName: site-pvc
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-site
labels:
app: nginx-site
spec:
containers:
- name: nginx
image: nginx:1.27.4-alpine
ports:
- containerPort: 80
volumeMounts:
# Serve the PVC content instead of the default nginx html folder content.
- name: site
mountPath: /usr/share/nginx/html
readOnly: true
volumes:
- name: site
persistentVolumeClaim:
claimName: site-pvc
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: refresh-ceid-upatras-nightly
spec:
# Run every day at 02:15
schedule: "15 2 * * *"
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: ubuntu
image: ubuntu:24.04
# Reuse the exact same script to refresh content nightly
command: ["/bin/bash", "/scripts/download.sh"]
volumeMounts:
- name: script
mountPath: /scripts
- name: site
mountPath: /data
volumes:
- name: script
configMap:
name: download-script
defaultMode: 0755
- name: site
persistentVolumeClaim:
claimName: site-pvc