A hands-on repository covering Docker and Kubernetes — built from real practice including all the mistakes, fixes, and lessons learned along the way.
| Component | Version |
|---|---|
| Docker Desktop | 29.2.0 |
| minikube | v1.38.1 |
| kubectl | v1.34.1 |
| Helm | 3.x |
| OS | Windows 11 + PowerShell |
# With NetworkPolicy support (recommended)
minikube start --driver=docker --cni=calico
# Enable addons
minikube addons enable ingress
minikube addons enable metrics-serverWindows tip: use
curl.exeinstead ofcurl(which is an alias forInvoke-WebRequest).
Direct cluster IP192.168.58.2is not reachable from the host — usekubectl port-forward.
minikube tunnelonly works withLoadBalancerservices, notNodePort.
container-practice/
├── build/ # Docker images
├── kubernetes/ # Kubernetes manifests
├── helm/ # Helm + RBAC
├── hpa-demo/ # Horizontal Pod Autoscaler
└── argocd/ # GitOps with ArgoCD
└── eks-wordpress-lab/ # AWS + EKS practice
└── gke-flask-app/ # GCP + GKE practice
Source files for a simple Python application and Dockerfiles.
app.py # Flask application
Dockerfile # Final multi-stage image
Dockerfile_prev # Previous version (for comparison)
requirements.txt # Python dependencies
docker build -t my-app:1.0 .
docker run -p 5000:5000 my-app:1.0kubectl create namespace dev
kubectl apply -f kubernetes/ -n dev| File | Description | Topic |
|---|---|---|
deployment.yml |
nginx Deployment, 3 replicas, RollingUpdate strategy | Foundations |
deployment-recreate.yml |
Deployment with Recreate strategy | Workloads |
service.yml |
ClusterIP Service for nginx | Foundations |
service-nodeport.yml |
NodePort Service (port 30080) | Networking |
ingress.yml |
Ingress: myapp.local/nginx and /app2 | Networking |
networkpolicy.yaml |
Allow traffic only from ingress-nginx namespace (requires Calico!) | Networking |
deployment-app2.yml |
httpd:2.4 for the second Ingress path | Networking |
configmap.yml |
ConfigMap with APP_ENV, DB_HOST, nginx.conf | Configuration |
secret.yml |
Secret with DB_PASSWORD and API_KEY (base64) | Configuration |
deployment-with-config.yml |
Deployment with env vars from ConfigMap/Secret | Configuration |
deployment-with-volume.yaml |
Deployment with volume mount (live reload!) | Configuration |
postgres-pvc.yaml |
PersistentVolumeClaim 1Gi for PostgreSQL | Storage |
postgres-secret.yaml |
Secret with PostgreSQL password | Storage |
postgres-deployment.yaml |
PostgreSQL Deployment with PVC | Storage |
job.yaml |
Job: db-migration (one-off task) | Workloads |
cronjob.yaml |
CronJob: backup every 2 minutes | Workloads |
daemonset.yaml |
DaemonSet log-collector with hostPath | Workloads |
statefulset.yaml |
StatefulSet postgres-ss, 2 replicas, headless service | Workloads |
my-first-app.yml |
First Pod (nginx) | Foundations |
my-first-service.yml |
First Service | Foundations |
my-config.yml |
Additional ConfigMap | Configuration |
my-ingress.yml |
Additional Ingress | Networking |
serviceaccount.yaml |
ServiceAccount for RBAC practice | RBAC |
⚠️ Demo secrets: the values insecret.ymlandpostgres-secret.yamlare example/demo credentials for learning only — they are not real and must not be reused. Never commit real secrets to git; use a secrets manager (e.g. AWS Secrets Manager, Sealed Secrets, External Secrets).
# Rolling Update
kubectl set image deployment nginx-deployment nginx=nginx:1.27 -n dev
kubectl rollout status deployment nginx-deployment -n dev
kubectl rollout history deployment nginx-deployment -n dev
kubectl rollout undo deployment nginx-deployment -n dev
# Scaling
kubectl scale deployment nginx-deployment --replicas=5 -n dev
# Test Ingress (Windows)
kubectl port-forward -n ingress-nginx service/ingress-nginx-controller 8080:80
curl.exe -H "Host: myapp.local" http://localhost:8080/nginx
# Live ConfigMap update (PowerShell)
$patch = '{"data":{"APP_ENV":"staging"}}'
kubectl patch configmap nginx-config -n dev --type merge -p $patch
# The file updates inside the pod within ~60s — no restart needed!
# StatefulSet DNS
# postgres-ss-0.postgres-headless.dev.svc.cluster.local
# postgres-ss-1.postgres-headless.dev.svc.cluster.localNote: NetworkPolicy only works with Calico CNI.
With the default bridge CNI, objects are created but rules are not enforced.
cd helm/
# Install bitnami nginx
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install my-nginx bitnami/nginx --namespace helm-demo --set service.type=ClusterIP
# Custom chart (in my-app/ directory)
helm install my-release ./my-app --namespace helm-demo
helm upgrade my-release ./my-app --set replicaCount=5
helm rollback my-release 1 -n helm-demo
helm history my-release -n helm-demoGotcha:
helm creategenerates extra files (httproute.yaml, hpa.yaml, serviceaccount.yaml).
Delete them or add all required sections to values.yaml — otherwisehelm lintwill fail.
| File | Description |
|---|---|
serviceaccount.yaml |
ServiceAccount app-service-account in namespace rbac-demo |
role.yaml |
Role pod-reader: get/list/watch pods, get/list deployments |
rolebinding.yaml |
RoleBinding: SA → Role (scoped to namespace rbac-demo) |
pod-with-sa.yaml |
Pod api-client for testing permissions from inside the cluster |
clusterrole-monitoring.yaml |
ClusterRole monitoring-role: read-only on nodes/pods/services/deployments |
kubectl create namespace rbac-demo
kubectl apply -f serviceaccount.yaml
kubectl apply -f role.yaml
kubectl apply -f rolebinding.yaml
# Test permissions
kubectl auth can-i get pods --as=system:serviceaccount:rbac-demo:app-service-account -n rbac-demo
# → yes
kubectl auth can-i delete pods --as=system:serviceaccount:rbac-demo:app-service-account -n rbac-demo
# → no
kubectl auth can-i get pods --as=system:serviceaccount:rbac-demo:app-service-account -n helm-demo
# → no (until ClusterRoleBinding is created)
# ClusterRole — access across all namespaces
kubectl apply -f clusterrole-monitoring.yaml
kubectl create clusterrolebinding monitoring-binding `
--clusterrole=monitoring-role `
--serviceaccount=rbac-demo:app-service-account
kubectl auth can-i get pods --as=system:serviceaccount:rbac-demo:app-service-account -n helm-demo
# → yesdeployment.yaml # php-apache with resource requests (required for HPA!)
hpa.yaml # HPA: min=1, max=10, target CPU=50%
kubectl create namespace hpa-demo
kubectl apply -f deployment.yaml -n hpa-demo
kubectl apply -f hpa.yaml -n hpa-demo
# Watch HPA in real time
kubectl get hpa -n hpa-demo -w
# Generate load
kubectl run load-generator --image=busybox --restart=Never -n hpa-demo `
-- /bin/sh -c "while true; do wget -q -O- http://php-apache.hpa-demo.svc.cluster.local; done"cpu: 0%/50% replicas=1 ← idle baseline
cpu: 113%/50% replicas=1 ← load starts
cpu: 113%/50% replicas=3 ← scale up (~15s)
cpu: 104%/50% replicas=6 ← scale up continues
cpu: 65%/50% replicas=7 ← stabilizing near target
cpu: 0%/50% replicas=7 ← load removed, cooldown begins
cpu: 0%/50% replicas=3 ← scale down step 1
cpu: 0%/50% replicas=1 ← back to minReplicas ✅
Formula: desiredReplicas = currentReplicas × (currentCPU / targetCPU)
Scale up is near-instant.
Scale down has a ~5 minute cooldown window — protection against flapping.
resources.requestsis mandatory — HPA does not work without it.
argocd-app.yaml # Application: argoproj/argocd-example-apps (guestbook)
argocd-nginx-app.yaml # Application: own repo (nginx-gitops)
kubectl create namespace argocd
kubectl apply -n argocd -f `
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Get admin password (PowerShell)
kubectl get secret argocd-initial-admin-secret -n argocd `
-o jsonpath='{.data.password}' | `
% { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) }
# Open UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# → https://localhost:8080 (admin / <password above>)# Edit argocd-nginx-app.yaml — set repoURL to your repository
kubectl apply -f argocd-nginx-app.yaml
kubectl get application nginx-gitops -n argocd -w# ❌ Don't do this — ArgoCD will revert it!
kubectl scale deployment nginx-gitops -n gitops-demo --replicas=0
# ✅ Correct way — change via Git
# Edit deployment.yaml: replicas: 2 → replicas: 4
git add .
git commit -m "scale: nginx 2 → 4 replicas"
git push
# ArgoCD detects the change (~3 min) and applies automatically# Test: manually kill all pods
kubectl scale deployment nginx-gitops -n gitops-demo --replicas=0
# ArgoCD restores INSTANTLY (Watch API, not polling!)
kubectl get pods -n gitops-demo -w
# → pods recreated in ~2 secondsUnknown Healthy ← ArgoCD reading the repo
OutOfSync Missing ← diff detected: resources absent in cluster
Synced Progressing ← manifests applied, pods starting
Synced Healthy ← everything running ✅
Polling every 3 min — detects new commits in Git.
Watch API (instant) — protects against manual changes in the cluster.
Not stored in this repo — deployed via Helm.
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus-stack prometheus-community/kube-prometheus-stack `
--namespace monitoring `
--set grafana.adminPassword=admin123 `
--set prometheus.prometheusSpec.retention=24h `
--set alertmanager.enabled=false
# Grafana UI
kubectl port-forward -n monitoring service/prometheus-stack-grafana 3000:80
# → http://localhost:3000 (admin / admin123)
# Prometheus UI
kubectl port-forward -n monitoring `
service/prometheus-stack-kube-prom-prometheus 9090:9090
# → http://localhost:9090# CPU by pod (minikube: cpu="total" filter is required!)
sum by (pod) (
rate(container_cpu_usage_seconds_total{
namespace="load-test", cpu="total", container!=""
}[2m])
)
# Container memory usage
container_memory_working_set_bytes{namespace="dev", container!=""}
# Running pods per namespace
count by (namespace) (kube_pod_info)
# Container restart count
kube_pod_container_status_restarts_total
Empty result in PromQL?
Check: 1) the Service exists, 2) load is actually running (kubectl top pods),
3) wait 2 minutes after starting load, 4) addcpu="total"filter on minikube.
# Cluster overview
kubectl get all -A
kubectl top nodes
kubectl top pods -n <namespace>
kubectl get events -n <namespace> --sort-by='.lastTimestamp'
# Diagnostics
kubectl describe pod <pod> -n <namespace>
kubectl logs <pod> -n <namespace> -f
kubectl exec -it <pod> -n <namespace> -- bash
# Cleanup
kubectl delete namespace <namespace> # deletes everything inside!
helm uninstall <release> -n <namespace>| Topic | Key Objects |
|---|---|
| Foundations | Pod, Namespace, Deployment, ReplicaSet, Service, Labels |
| Workloads | RollingUpdate, Recreate, Rollback, Revision History |
| Networking | ClusterIP, NodePort, Ingress, NetworkPolicy (Calico) |
| Storage | emptyDir, PVC/PV, StatefulSet, Job, CronJob, DaemonSet |
| Configuration | ConfigMap, Secret, env vars vs volume mount |
| Helm | Chart, Release, Values, Upgrade, Rollback |
| RBAC | Role, ClusterRole, RoleBinding, ServiceAccount |
| Monitoring | Prometheus, Grafana, PromQL, PrometheusRule |
| GitOps | ArgoCD, Application, selfHeal, automated sync |
| Autoscaling | HPA, scale up/down, cooldown, flapping protection |