This lab documents how to build and troubleshoot a local Kubernetes cluster on macOS using kind, Docker, and kubectl.
The purpose of this lab is to practice Kubernetes administration, application deployment, service exposure, log analysis, troubleshooting, and operational workflows in a local environment.
The objective of this lab is to create a local Kubernetes environment on macOS and practice the most common Kubernetes support and troubleshooting tasks.
By the end of this lab, I should be able to:
kubectl to inspect cluster resources.High-level architecture:
macOS Host
│
├── Docker Desktop
│ │
│ └── kind Kubernetes Cluster
│ │
│ ├── Control Plane Node
│ │
│ └── Worker Node(s)
│
└── kubectl CLI
│
└── Manages Kubernetes resources
Application flow:
User / Browser
│
└── localhost
│
└── kubectl port-forward
│
└── Kubernetes Service
│
└── Pod running containerized application
kind is a tool for running local Kubernetes clusters using Docker container nodes. It is commonly used for local development and testing. Kubernetes also lists kind as one of the local Kubernetes options and notes that it requires Docker or Podman.
Sources: kind Quick Start, Kubernetes Install Tools
Before starting, confirm that the Mac has:
Check Homebrew:
brew --version
Check Docker:
docker --version
docker ps
If docker ps fails, start Docker Desktop first.
Install kubectl:
brew install kubectl
Validate:
kubectl version --client
Install kind:
brew install kind
Validate:
kind version
Optional tools:
brew install jq
brew install watch
Useful validation:
which kubectl
which kind
which docker
Create a basic cluster:
kind create cluster --name devops-lab
Expected result:
Creating cluster "devops-lab" ...
Ensuring node image ...
Preparing nodes ...
Writing configuration ...
Starting control-plane ...
Installing CNI ...
Installing StorageClass ...
Check kind clusters:
kind get clusters
Check Docker containers created by kind:
docker ps
The cluster context should be created automatically.
Check current context:
kubectl config current-context
Expected context:
kind-devops-lab
Check cluster information:
kubectl cluster-info
Check nodes:
kubectl get nodes
Check nodes with more details:
kubectl get nodes -o wide
Check all pods across all namespaces:
kubectl get pods -A
Check Kubernetes system components:
kubectl get pods -n kube-system
Describe the node:
kubectl describe node devops-lab-control-plane
Troubleshooting logic:
Ready, check Docker Desktop resources.kubectl cannot connect, confirm the current context.Create a short alias for kubectl.
For Zsh on macOS:
echo 'alias k=kubectl' >> ~/.zshrc
source ~/.zshrc
For Bash:
echo 'alias k=kubectl' >> ~/.bashrc
source ~/.bashrc
Test:
k get nodes
Enable autocomplete for Zsh:
echo 'source <(kubectl completion zsh)' >> ~/.zshrc
echo 'alias k=kubectl' >> ~/.zshrc
echo 'compdef __start_kubectl k' >> ~/.zshrc
source ~/.zshrc
Useful daily aliases:
alias kgp='kubectl get pods'
alias kgpa='kubectl get pods -A'
alias kgn='kubectl get nodes -o wide'
alias kgs='kubectl get svc'
alias kgsa='kubectl get svc -A'
alias kge='kubectl get events --sort-by=.metadata.creationTimestamp'
alias kgea='kubectl get events -A --sort-by=.metadata.creationTimestamp'
alias kd='kubectl describe'
alias kdp='kubectl describe pod'
alias kl='kubectl logs'
alias klf='kubectl logs -f'
Check kubeconfig:
ls -l ~/.kube/config
kubectl config get-contexts
kubectl config current-context
Secure kubeconfig permissions:
chmod 600 ~/.kube/config
Create a namespace:
kubectl create namespace web-lab
Set it as the default namespace for the current context:
kubectl config set-context --current --namespace=web-lab
Deploy NGINX:
kubectl create deployment nginx-demo --image=nginx:latest
Check deployment:
kubectl get deployments
Check pods:
kubectl get pods -o wide
Describe the deployment:
kubectl describe deployment nginx-demo
Describe the pod:
kubectl describe pod <pod-name>
Check logs:
kubectl logs <pod-name>
Expected result:
Deployment exists
Pod is Running
Container image is nginx:latest
Expose the deployment as a ClusterIP service:
kubectl expose deployment nginx-demo --port=80 --target-port=80 --name=nginx-demo-service
Check service:
kubectl get svc
Describe service:
kubectl describe svc nginx-demo-service
Check endpoints:
kubectl get endpoints nginx-demo-service
Expected result:
The service should have endpoints pointing to the NGINX pod IP.
Troubleshooting logic:
targetPort.Port forward local port 8080 to service port 80:
kubectl port-forward svc/nginx-demo-service 8080:80
Open another terminal and test:
curl -I http://localhost:8080
Expected result:
HTTP/1.1 200 OK
Server: nginx
Open in browser:
http://localhost:8080
Stop port forwarding with:
CTRL + C
Troubleshooting logic:
Scale the deployment to 3 replicas:
kubectl scale deployment nginx-demo --replicas=3
Check pods:
kubectl get pods -o wide
Check deployment:
kubectl get deployment nginx-demo
Check endpoints:
kubectl get endpoints nginx-demo-service
Expected result:
Three NGINX pods should be running.
The service should have three endpoints.
Troubleshooting logic:
Update the image:
kubectl set image deployment/nginx-demo nginx=nginx:1.25
Check rollout status:
kubectl rollout status deployment/nginx-demo
Check rollout history:
kubectl rollout history deployment/nginx-demo
Check pods:
kubectl get pods
Describe deployment:
kubectl describe deployment nginx-demo
Troubleshooting logic:
kubectl describe pod.Roll back to the previous version:
kubectl rollout undo deployment/nginx-demo
Check rollout status:
kubectl rollout status deployment/nginx-demo
Check deployment image:
kubectl describe deployment nginx-demo
Expected result:
Deployment should return to the previous image version.
Create an evidence folder locally:
mkdir -p ~/k8s-lab-evidence
Save pod list:
kubectl get pods -o wide > ~/k8s-lab-evidence/pods.txt
Save services:
kubectl get svc -o wide > ~/k8s-lab-evidence/services.txt
Save events:
kubectl get events --sort-by=.metadata.creationTimestamp > ~/k8s-lab-evidence/events.txt
Save deployment YAML:
kubectl get deployment nginx-demo -o yaml > ~/k8s-lab-evidence/nginx-demo-deployment.yaml
Save service YAML:
kubectl get svc nginx-demo-service -o yaml > ~/k8s-lab-evidence/nginx-demo-service.yaml
Save pod logs:
POD_NAME=$(kubectl get pods -l app=nginx-demo -o jsonpath='{.items[0].metadata.name}')
kubectl logs "$POD_NAME" > ~/k8s-lab-evidence/nginx-demo-pod.log
List evidence:
ls -lah ~/k8s-lab-evidence
Important: Review files before committing them to GitHub. Do not commit secrets, tokens, private keys, or sensitive kubeconfig files.
Create a broken deployment with a fake image:
kubectl create deployment broken-image --image=nginx:this-tag-does-not-exist
Check pod status:
kubectl get pods
Expected issue:
ImagePullBackOff
Investigate:
kubectl describe pod <broken-pod-name>
kubectl get events --sort-by=.metadata.creationTimestamp
What to look for:
Failed to pull image
Image not found
ErrImagePull
Back-off pulling image
Fix it:
kubectl set image deployment/broken-image nginx=nginx:latest
Validate:
kubectl rollout status deployment/broken-image
kubectl get pods
Clean up:
kubectl delete deployment broken-image
Documented root cause:
The pod failed because the container image tag did not exist. Kubernetes could not pull the image from the container registry.
Create a pod that exits immediately:
kubectl run crash-demo --image=busybox --restart=Never -- /bin/sh -c "exit 1"
Check pod:
kubectl get pods
Describe pod:
kubectl describe pod crash-demo
Check logs:
kubectl logs crash-demo
Note: Because this is a standalone pod with restart=Never, it may show Error instead of CrashLoopBackOff.
To create a real CrashLoopBackOff using a Deployment:
kubectl create deployment crashloop-demo --image=busybox -- /bin/sh -c "exit 1"
Check:
kubectl get pods
Investigate:
kubectl describe pod <crashloop-pod-name>
kubectl logs <crashloop-pod-name>
kubectl logs <crashloop-pod-name> --previous
kubectl get events --sort-by=.metadata.creationTimestamp
Fix it by changing the command:
kubectl delete deployment crashloop-demo
kubectl create deployment crashloop-demo --image=busybox -- /bin/sh -c "sleep 3600"
Validate:
kubectl get pods
Clean up:
kubectl delete deployment crashloop-demo
kubectl delete pod crash-demo
Documented root cause:
The container repeatedly exited with code 1, causing Kubernetes to restart it until it entered CrashLoopBackOff.
Create a service with a selector that does not match any pod:
kubectl create service clusterip broken-service --tcp=80:80
Edit the service selector:
kubectl edit svc broken-service
Set selector to something that does not match existing pods:
selector:
app: does-not-exist
Check endpoints:
kubectl get endpoints broken-service
Expected result:
No endpoints
Investigate:
kubectl describe svc broken-service
kubectl get pods --show-labels
Fix by matching the selector to the NGINX pod label.
Check labels:
kubectl get pods --show-labels
Edit service:
kubectl edit svc broken-service
Set selector:
selector:
app: nginx-demo
Validate:
kubectl get endpoints broken-service
Clean up:
kubectl delete svc broken-service
Documented root cause:
The service had no endpoints because the service selector did not match any pod labels.
Problem:
The application is expected to respond through localhost, but curl fails.
Commands to investigate:
kubectl get pods
kubectl get svc
kubectl get endpoints nginx-demo-service
kubectl describe svc nginx-demo-service
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl port-forward svc/nginx-demo-service 8080:80
curl -v http://localhost:8080
Troubleshooting logic:
1. Confirm pod is Running.
2. Confirm service exists.
3. Confirm service has endpoints.
4. Confirm targetPort matches container port.
5. Confirm port-forward is running.
6. Test localhost with curl.
7. Check logs if HTTP response is not expected.
Possible root causes:
Pod is not running
Service selector mismatch
Wrong service port
Wrong target port
Port-forward not running
Local port already in use
Application inside container is not responding
Possible fixes:
Restart or fix pod
Correct service selector
Correct targetPort
Use a different local port
Check application logs
Redeploy application
Delete the namespace and all resources inside it:
kubectl delete namespace web-lab
Delete the kind cluster:
kind delete cluster --name devops-lab
Validate:
kind get clusters
docker ps