Portfolio
Loading Shiham's portfolio
Preparing the latest projects, skills, and contact paths with a quiet production-ready polish.
Initializing portfolio
Projects · Skills · Contact
Portfolio
Preparing the latest projects, skills, and contact paths with a quiet production-ready polish.
Initializing portfolio
Projects · Skills · Contact
A hands-on DevOps playground for developers learning to containerize an Express API and run replicated workloads on Kubernetes with Docker and Minikube.
K8s Playground is a personal learning project created to practice the complete path from a small web service to a containerized Kubernetes workload. It is aimed at developers learning how Docker images, local clusters, Deployments, Pods, ReplicaSets, Services, health probes, scaling, and self-healing fit together.
The project exposes a minimal Express API with a root response plus dedicated readiness and liveness endpoints. A Kubernetes Deployment runs two replicas, injects each Pod name through the downward API, applies CPU and memory requests and limits, and connects the Pods through a NodePort Service for local access and routing experiments.
The application is packaged with a non-root Node.js Alpine container, can be run through Docker Compose, and includes a shell workflow that builds and pushes the image to Docker Hub before applying the manifests. I implemented the API, container configuration, Kubernetes resources, deployment script, and documentation independently.
Related projects
Jan 2026 – Jun 2026
A multi-vendor commerce platform connecting buyers, sellers, and administrators through dedicated storefronts, dashboards, payments, chat, and personalized discovery.
A single Express service is packaged as a Docker image and deployed to a local Minikube cluster. A Kubernetes Deployment maintains two Pod replicas with resource controls and health probes, while a NodePort Service routes traffic to ready Pods. Docker Hub supplies the image, and a shell script coordinates build, push, and manifest application.
A beginner-friendly Kubernetes learning playground built with Node.js, Express, Docker, Docker Hub, Minikube, and Kubernetes.
This project contains a small Express API that is packaged into a Docker image and deployed to a local Kubernetes cluster. It is meant for learning the full flow:
Use this shortest working flow when you already have Docker Desktop, Minikube, and kubectl installed.
minikube start --driver=docker
minikube status
kubectl get nodes
kubectl apply -f k8s/
kubectl get pods -w
kubectl port-forward service/k8s-playground-api-service 3000:3000
What these commands do:
minikube start --driver=docker starts a local Kubernetes cluster using Docker Desktop.minikube status checks that the cluster is running.kubectl get nodes checks that kubectl can talk to Minikube.kubectl apply -f k8s/ creates the Deployment and Service.kubectl get pods -w watches the Pods until they are running.kubectl port-forward service/k8s-playground-api-service 3000:3000 exposes the Service on your local machine.When the Pods are running, keep the port-forward terminal open and open:
http://localhost:3000
Press Ctrl+C to stop watching Pods or to stop port-forwarding.
The normal Minikube start command for this project is:
minikube start --driver=docker
If Minikube gets stuck downloading images, has preload issues, or runs into cache/network problems on Windows with Docker Desktop, this fallback command worked successfully in this setup:
minikube start --driver=docker --kubernetes-version=v1.33.3 --preload=false --cache-images=false --memory=6144 --cpus=4 --base-image=docker.io/kicbase/stable:v0.0.48
What the flags mean:
--kubernetes-version=v1.33.3 uses a specific Kubernetes version instead of letting Minikube choose one automatically.--preload=false avoids the large Minikube preload download.--cache-images=false avoids problems caused by Minikube's local image cache.--memory=6144 gives the Minikube cluster about 6 GB of memory.--cpus=4 gives the Minikube cluster four CPU cores.--base-image=docker.io/kicbase/stable:v0.0.48 uses the fallback Minikube base image that Docker can pull.If Docker can pull the fallback base image, it can help confirm that Docker networking is working:
docker pull docker.io/kicbase/stable:v0.0.48
This project uses Minikube with the Docker driver.
That means Docker Desktop runs the container runtime for Minikube. In Docker Desktop, you may see a container named:
minikube
That container is the local Kubernetes node.
Docker Desktop also has its own Kubernetes feature in the Docker Desktop settings. That is separate from Minikube. If you are following this README, focus on the Minikube cluster and make sure your kubectl context is set to Minikube.
Check the current context:
kubectl config current-context
If needed, switch to Minikube:
kubectl config use-context minikube
The Minikube cluster is easiest to view with:
minikube dashboard
This opens a Kubernetes dashboard for the Minikube cluster, where you can see Deployments, Pods, ReplicaSets, Services, and events.
After Minikube starts successfully, kubectl get nodes should show a ready Minikube node:
kubectl get nodes
Expected:
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane ... v1.33.3
After applying the manifests, kubectl get pods should show two running Pods:
kubectl get pods
Expected:
NAME READY STATUS RESTARTS AGE
k8s-playground-api-xxxxx 1/1 Running 0 ...
k8s-playground-api-yyyyy 1/1 Running 0 ...
When you open http://localhost:3000, the browser should show JSON like this:
{
"message": "Hello from a container!",
"service": "hello-node",
"pod": "k8s-playground-api-c7cfb69c7-j9nrd",
"time": "2026-06-16T10:37:33.818Z"
}
The exact Pod name and time will be different on your machine.
The Express app runs normally with Node.js, but Kubernetes runs containers. The Dockerfile describes how to build a container image for the app. That image includes Node.js, installs the project dependencies, copies the app files, exposes port 3000, and starts the server with npm start.
Docker Hub is a container image registry. You can build an image on your machine and push it to Docker Hub. Kubernetes can then pull that image when it creates Pods.
In this project, the Kubernetes Deployment uses:
image: shihamahamed/k8s-playground-api
That means Kubernetes will try to pull the shihamahamed/k8s-playground-api image from Docker Hub.
Minikube runs a small Kubernetes cluster on your computer. It is useful for learning because you can practice Kubernetes concepts locally before using a cloud provider.
This project uses Minikube with the Docker driver:
minikube start --driver=docker
A Deployment tells Kubernetes how to run your application. It defines the container image, how many replicas should run, which ports the container exposes, environment variables, resource requests and limits, and health checks.
This project has a Deployment named:
k8s-playground-api
The Deployment is defined in:
k8s/deployment.yaml
A Pod is the smallest deployable unit in Kubernetes. In this project, each Pod runs one container with the Express API.
The Deployment creates Pods for you. You usually do not create these Pods manually. Instead, you tell the Deployment how many replicas you want.
A ReplicaSet makes sure the requested number of Pods are running. Deployments manage ReplicaSets automatically.
For example, this project starts with:
replicas: 2
That means Kubernetes should keep two API Pods running.
Pods can be replaced at any time, so their IP addresses are not stable. A Service gives your Pods a stable network endpoint.
This project has a Service named:
k8s-playground-api-service
The Service is defined in:
k8s/service.yaml
The Service uses NodePort. A NodePort Service exposes the app on a port on the Kubernetes node. In Minikube, you can ask Minikube for the service URL.
minikube service k8s-playground-api-service --url
For simple local testing, this README also uses kubectl port-forward, which maps the Kubernetes Service to localhost:3000.
A readiness probe tells Kubernetes whether the app is ready to receive traffic.
This project uses:
/readyz
If the readiness check fails, Kubernetes keeps the Pod running but stops sending traffic to it through the Service.
A liveness probe tells Kubernetes whether the app is still alive.
This project uses:
/healthz
If the liveness check fails repeatedly, Kubernetes restarts the container.
The Deployment passes the Pod name into the app using the POD_NAME environment variable:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
The / endpoint returns this value, which helps you see which Pod handled a request.
Scaling means changing how many copies of the app are running. For example, you can scale from two Pods to four Pods:
kubectl scale deployment k8s-playground-api --replicas=4
Kubernetes tries to keep the desired state running. If you delete one Pod from a Deployment, Kubernetes creates another Pod to replace it.
The Service sends traffic to healthy, ready Pods that match its selector. Because this project has multiple replicas, the Service can route requests to different Pods.
You will use kubectl to inspect the cluster. The most useful commands while learning are:
kubectl get pods
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl get events --sort-by=.lastTimestamp
These commands help you understand what Kubernetes is doing and why something may not be working.
k8s-playground/
|-- index.js
|-- package.json
|-- package-lock.json
|-- Dockerfile
|-- docker-compose.yaml
|-- deploy.sh
|-- k8s/
| |-- deployment.yaml
| `-- service.yaml
|-- .dockerignore
|-- .gitignore
`-- README.md
index.jsThe main Express API file. It creates the server, defines the routes, and listens on port 3000 by default.
Important routes:
GET /GET /readyzGET /healthzpackage.jsonDefines the Node.js project metadata, dependencies, and scripts.
Important scripts:
{
"dev": "node --watch index.js",
"start": "node index.js",
"deploy": "sh deploy.sh"
}
DockerfileDefines how to build the Docker image for the Express API. It uses node:18-alpine, installs dependencies with npm ci --omit=dev, copies the app files, exposes port 3000, and runs npm start.
docker-compose.yamlRuns the API with Docker Compose. It builds the image from the local Dockerfile, maps port 3000, mounts the current folder into the container, and runs the dev script.
deploy.shBuilds the Docker image, pushes it to Docker Hub, applies the Kubernetes manifests, and prints useful cluster information.
The script uses:
shihamahamed/k8s-playground-api:latest
k8s/deployment.yamlCreates the Kubernetes Deployment. It defines:
k8s-playground-api2shihamahamed/k8s-playground-api3000NODE_ENV environment variablePOD_NAME environment variable from Kubernetes metadata/readyz/healthzk8s/service.yamlCreates the Kubernetes Service. It defines:
k8s-playground-api-serviceNodePort30003000app: k8s-playground-api.dockerignoreTells Docker which files and folders to leave out of the build context. This keeps the Docker build cleaner and avoids copying unnecessary files such as node_modules.
.gitignoreTells Git which files and folders should not be committed. This project ignores node_modules.
GET /Returns a JSON response with a message, service name, Pod name, and timestamp.
Example response when running in Kubernetes:
{
"message": "Hello from a container!",
"service": "hello-node",
"pod": "k8s-playground-api-6f8c9d7d6b-abc12",
"time": "2026-06-16T10:30:00.000Z"
}
When running locally without Kubernetes, pod will usually be:
"unknown"
That happens because POD_NAME is provided by Kubernetes only when the app is running inside a Pod.
GET /readyzReadiness endpoint used by Kubernetes.
Expected response:
ready
GET /healthzLiveness endpoint used by Kubernetes.
Expected response:
ok
Install these tools before running the project:
Check the main tools:
node --version
npm --version
docker --version
minikube version
kubectl version --client
Make sure Docker Desktop is running before starting Minikube with the Docker driver.
Use this when you want to test only the Express app.
npm install
This installs the Express dependency listed in package.json.
npm run dev
This starts the app with Node.js watch mode. The server restarts when files change.
Open:
http://localhost:3000
Test the health endpoints:
http://localhost:3000/readyz
http://localhost:3000/healthz
npm start
This runs:
node index.js
Use this when you want to run the app the same way the Docker container runs it.
Use this when you want to test the container before using Kubernetes.
docker build -t k8s-playground-api .
This creates a local Docker image named k8s-playground-api.
docker run --rm -p 3000:3000 --name k8s-playground-api k8s-playground-api
This maps your computer's port 3000 to the container's port 3000.
Open:
http://localhost:3000
Stop the container with Ctrl+C.
docker compose up --build
This builds the image and starts the service defined in docker-compose.yaml.
Open:
http://localhost:3000
docker compose down
This stops and removes the Compose container.
Use this when you want to practice Kubernetes Deployments, Pods, Services, probes, scaling, and debugging.
minikube start --driver=docker
This starts a local Kubernetes cluster using Docker Desktop.
minikube status
This confirms that Minikube is running.
kubectl get nodes
You should see a Minikube node with status Ready.
kubectl apply -f k8s/
This creates or updates the Deployment and Service from the k8s/ folder.
kubectl get pods
You should eventually see two Pods with status Running.
For more detail:
kubectl get pods -o wide
kubectl get svc
Look for:
k8s-playground-api-service
kubectl port-forward service/k8s-playground-api-service 3000:3000
This forwards traffic from your computer to the Kubernetes Service.
Keep this terminal open while testing. In another terminal, open:
http://localhost:3000
You can also test with curl:
curl.exe http://localhost:3000
curl.exe http://localhost:3000/readyz
curl.exe http://localhost:3000/healthz
There are three separate ideas to understand.
docker build -t shihamahamed/k8s-playground-api:latest .
This creates the image on your own computer.
docker login
docker push shihamahamed/k8s-playground-api:latest
This uploads the image to Docker Hub so other systems can pull it.
The Deployment contains:
image: shihamahamed/k8s-playground-api
When Kubernetes creates a Pod, it asks the container runtime to pull that image. If no tag is provided, Docker/Kubernetes treat it like latest.
So this:
image: shihamahamed/k8s-playground-api
is effectively:
image: shihamahamed/k8s-playground-api:latest
If the image does not exist on Docker Hub, or if it is private and Kubernetes does not have permission, the Pod may show ImagePullBackOff or ErrImagePull.
The project includes:
npm run deploy
This runs:
sh deploy.sh
On Windows, this requires a shell that can run .sh files, such as Git Bash or WSL. If you are using plain PowerShell and sh is not available, run the Docker and Kubernetes commands manually from this README.
Show all common resources:
kubectl get all
Show Pods:
kubectl get pods
Show Pods with node and IP information:
kubectl get pods -o wide
Describe a specific Pod:
kubectl describe pod <pod-name>
Read logs from a Pod:
kubectl logs <pod-name>
Show recent cluster events:
kubectl get events --sort-by=.lastTimestamp
Describe the Deployment:
kubectl describe deployment k8s-playground-api
Describe the Service:
kubectl describe svc k8s-playground-api-service
Start with the current Deployment:
kubectl get pods
Scale the app to four replicas:
kubectl scale deployment k8s-playground-api --replicas=4
Check the new Pods:
kubectl get pods
Scale back to two replicas:
kubectl scale deployment k8s-playground-api --replicas=2
Check again:
kubectl get pods
Kubernetes will create or remove Pods until the actual state matches the requested replica count.
Get the current Pods:
kubectl get pods
Delete one Pod:
kubectl delete pod <pod-name>
Watch Kubernetes recreate it:
kubectl get pods -w
Because the Deployment wants two replicas, Kubernetes creates a replacement Pod automatically.
Press Ctrl+C to stop watching.
Because the Deployment runs two replicas, the Service can send requests to different Pods.
Browser refresh may keep showing the same Pod because browsers often reuse connections. Also, kubectl port-forward service/k8s-playground-api-service 3000:3000 may select one backend Pod for the forwarded connection, so it is not always the best way to prove load balancing.
To better test Service routing, get a Minikube Service URL:
minikube service k8s-playground-api-service --url
Minikube may print a URL like:
http://127.0.0.1:<PORT>
On some Windows Docker-driver setups, Minikube may keep that terminal open while it creates a tunnel. If that happens, leave it running and use a second terminal for the curl test.
In another terminal, use that URL for repeated requests:
1..20 | ForEach-Object { curl.exe http://127.0.0.1:<PORT> }
Replace <PORT> with the port from the minikube service output.
You can also store the URL in a variable:
$url = minikube service k8s-playground-api-service --url
1..20 | ForEach-Object { curl.exe $url }
Look at the pod value in the JSON response. Seeing different Pod names proves that the Service is routing traffic to multiple Pods.
Open the Kubernetes dashboard:
minikube dashboard
This opens a visual UI for exploring:
The dashboard is helpful while learning because you can connect the YAML files to the resources Kubernetes creates.
Delete the Kubernetes resources created from the manifests:
kubectl delete -f k8s/
This removes the Deployment and Service from the cluster.
Stop Minikube:
minikube stop
Stopping Minikube turns off the local cluster but keeps its data. You can start it again later.
Delete Minikube:
minikube delete
Deleting Minikube removes the local cluster completely. Use this when you want a fresh start or no longer need the local cluster.
If Minikube uses the Docker driver, Docker Desktop must be running first.
Check Docker:
docker version
Then start Minikube again:
minikube start --driver=docker
Sometimes Minikube needs time to download images or cluster components.
Check status:
minikube status
If it keeps failing, restart Docker Desktop and try again:
minikube stop
minikube start --driver=docker
If the normal start command still gets stuck, try the fallback command in the "Windows / Docker Desktop Minikube Fallback Command" section.
Proxy environment variables can break Minikube or Docker image pulls if they point to a proxy that does not really exist.
Do not set this unless it is a real valid proxy for your host:
HTTP_PROXY=http://http.docker.internal:3128
To clear proxy variables from the current PowerShell session, run:
Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:NO_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:http_proxy -ErrorAction SilentlyContinue
Remove-Item Env:https_proxy -ErrorAction SilentlyContinue
Remove-Item Env:no_proxy -ErrorAction SilentlyContinue
Get-ChildItem Env:*proxy*
Get-ChildItem Env:*proxy* should show no unexpected proxy variables.
If Minikube's local state is badly stuck, you can fully remove Minikube state and flush DNS:
minikube delete --all --purge
ipconfig /flushdns
minikube delete --all --purge removes Minikube clusters, profiles, and cached state. Use it when you want a clean Minikube start.
If Docker can pull the fallback Minikube base image, that is a good sign that Docker can reach Docker Hub:
docker pull docker.io/kicbase/stable:v0.0.48
ContainerCreatingThis can happen while Kubernetes is pulling the image or setting up the container.
Inspect the Pod:
kubectl describe pod <pod-name>
Check events:
kubectl get events --sort-by=.lastTimestamp
ImagePullBackOff or ErrImagePullThis usually means Kubernetes cannot pull the Docker image.
Common causes:
Inspect the Pod:
kubectl describe pod <pod-name>
Verify the image exists on Docker Hub:
docker pull shihamahamed/k8s-playground-api:latest
kubectl port-forward says the Pod is not runningThe Service forwards to Pods that must exist and be ready.
Check Pod status:
kubectl get pods
Inspect a failing Pod:
kubectl describe pod <pod-name>
Read logs:
kubectl logs <pod-name>
kubectl context is not pointing to MinikubeCheck the current context:
kubectl config current-context
Switch to Minikube:
kubectl config use-context minikube
Update the Minikube context:
minikube update-context
Check nodes again:
kubectl get nodes
Check Minikube:
minikube status
Check cluster events:
kubectl get events --sort-by=.lastTimestamp
Restarting Minikube can help during local learning:
minikube stop
minikube start --driver=docker
Compare local logs and Kubernetes logs.
Local:
npm start
Kubernetes:
kubectl get pods
kubectl logs <pod-name>
kubectl describe pod <pod-name>
If the Pod is running but traffic does not work, check the Service:
kubectl describe svc k8s-playground-api-service
POD_NAME environment variable shows which Pod handled a request.This project is intentionally small, which makes it useful for university review, demos, and future Kubernetes practice.
The case-study preview is collapsed. Activate the button to make the full content available.
Aug 2025 – Oct 2025
A marketplace for verified Sri Lankan homestays, supporting guest bookings, host onboarding, admin approval, and test-mode payments.
Oct 2025 – Nov 2025
A full-stack developer Q&A platform where programmers can ask, answer, vote, save questions, explore tags, and generate AI-assisted responses.