12 Dec 2025
After setting up my K3s cluster (see my previous post on K3s installation), I deployed my first real application: a distributed voting app with multiple microservices. This project demonstrates key Kubernetes concepts including deployments, services, persistent storage, and inter-container communication.
This wasn't just following a tutorial - I adapted a Docker Compose application to run on my home lab Kubernetes cluster, managing it remotely from my workstation. The complete code is available in my GitHub repository.
The voting application consists of five microservices:
Data Flow:
Before starting, ensure you have:
On your workstation (not the server), create a project directory:
mkdir -p ~/projects/voting-app-k8s
cd ~/projects/voting-app-k8s
Initialize Git from the start to maintain version control:
git init
Create the repository on GitHub (make it public for portfolio purposes), then connect your local repository:
git remote add origin https://YOUR-USERNAME@github.com/YOUR-USERNAME/voting-app-kubernetes.git
Important: Include your username in the URL (https://YOUR-USERNAME@github.com/...) to avoid authentication issues.
I created nine YAML files total: five deployments and four services. Let me walk through the key components.
Before diving into the files, it's important to understand these two Kubernetes objects:
Deployments: Define how to run your containers
Services: Define how to access your containers
Namespaces organize resources and prevent conflicts with other applications:
kubectl create namespace appvotacion
Verify it was created:
kubectl get namespaces
Create db-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: appvotacion
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: postgres:15-alpine
env:
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
value: "postgres"
ports:
- containerPort: 5432
Key points:
Create db-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: db
namespace: appvotacion
spec:
type: ClusterIP
selector:
app: db
ports:
- port: 5432
targetPort: 5432
Key points:
Create redis-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: appvotacion
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
Create redis-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: appvotacion
spec:
type: ClusterIP
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Create worker-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
namespace: appvotacion
spec:
replicas: 1
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
containers:
- name: worker
image: dockersamples/examplevotingapp_worker
Note: The worker doesn't need a service because nothing connects to it - it connects to Redis and PostgreSQL.
Create vote-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote
namespace: appvotacion
spec:
replicas: 1
selector:
matchLabels:
app: vote
template:
metadata:
labels:
app: vote
spec:
containers:
- name: vote
image: dockersamples/examplevotingapp_vote
ports:
- containerPort: 80
Create vote-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: vote
namespace: appvotacion
spec:
type: NodePort
selector:
app: vote
ports:
- port: 80
targetPort: 80
nodePort: 31000
Key point: "NodePort" exposes this service outside the cluster at port 31000.
Create result-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: result
namespace: appvotacion
spec:
replicas: 1
selector:
matchLabels:
app: result
template:
metadata:
labels:
app: result
spec:
containers:
- name: result
image: dockersamples/examplevotingapp_result
ports:
- containerPort: 80
Create result-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: result
namespace: appvotacion
spec:
type: NodePort
selector:
app: result
ports:
- port: 80
targetPort: 80
nodePort: 31001
Before deploying, commit your YAML files:
git add .
git commit -m "Add voting app Kubernetes manifests"
git push -u origin main
From your project directory, apply all YAML files at once:
kubectl apply -f . --namespace=appvotacion
This command applies every YAML file in the current directory to the "appvotacion" namespace.
Check all resources were created:
kubectl get all -n appvotacion
You should see:
Verify all pods are running:
kubectl get pods -n appvotacion
All pods should show "STATUS: Running" and "READY: 1/1".
To see which node each pod is running on:
kubectl get pods -n appvotacion -o wide
The "NODE" column shows where each pod is running. Since I have a single-node cluster, all pods run on "master".
The voting and result services are exposed via NodePort, making them accessible from outside the cluster.
Access the applications in your browser:
http://YOUR-SERVER-IP:31000http://YOUR-SERVER-IP:31001
Test the application:
ClusterIP (db, redis):
NodePort (vote, result):
LoadBalancer (not used here):
Kubernetes provides built-in DNS for service discovery. When the worker needs to connect to Redis, it simply uses the hostname "redis" - Kubernetes resolves this to the correct ClusterIP automatically.
This is why our services are named "db" and "redis" - these names become DNS entries within the cluster.
Using kubectl apply -f . is efficient for applying entire directories of manifests. However, you can also apply files individually:
kubectl apply -f db-deployment.yaml -n appvotacion
kubectl apply -f db-service.yaml -n appvotacion
Or specify files explicitly:
kubectl apply -f db-deployment.yaml -f db-service.yaml -n appvotacion
Setting the default namespace avoids typing "-n appvotacion" every time:
kubectl config set-context --current --namespace=appvotacion
After this, "kubectl get pods" automatically queries the "appvotacion" namespace.
Check pod status and events:
kubectl describe pod POD-NAME -n appvotacion
Look at the "Events" section for error messages.
Verify the service was created:
kubectl get services -n appvotacion
Check that NodePort services show the correct ports (31000, 31001).
View logs from a specific pod:
kubectl logs POD-NAME -n appvotacion
Add "-f" to follow logs in real-time:
kubectl logs -f POD-NAME -n appvotacion
To remove the entire application:
kubectl delete namespace appvotacion
This deletes everything in the namespace: deployments, pods, services, and any other resources.
This project taught me several important Kubernetes concepts:
Future improvements I'm planning:
Deploying this multi-container application demonstrated real-world Kubernetes usage beyond simple tutorials. I managed the entire workflow from my workstation - writing YAML files, committing to Git, and deploying to my home lab cluster.
This hands-on experience with deployments, services, and namespaces provides a foundation for more complex projects. All code is available in my GitHub repository for reference.