We will deploy the Docker sample voting app to understand all three service types in one real example from https://github.com/dockersamples/example-voting-app

The Three Service Types
ClusterIP
Default service type and Internal only. No external access.
Used for redis and db — only other pods inside the cluster need to reach them.
spec:
type: ClusterIP
selector:
app: redis
ports:
- port: 6379
targetPort: 6379Pods find each other by service name — voting-app connects to redis:6379, not an IP. The service name is the DNS hostname inside the cluster.
NodePort
Opens a port on every node in the range 30000-32767. Accessible from outside the cluster via any node IP.
spec:
type: NodePort
selector:
app: voting-app
ports:
- port: 80 # service port inside cluster
targetPort: 80 # container port
nodePort: 31000 # external port on every nodeHit any node IP on that port:
http://172.16.77.10:31000
http://172.16.77.11:31000
http://172.16.77.12:31000All three work — the service load balances to the correct pod regardless of which node you hit.
LoadBalancer
Builds on top of NodePort. On k3s, ServiceLB automatically assigns all node IPs. In cloud (AWS/GKE/AKS) it provisions a real external load balancer with a single public IP.
spec:
type: LoadBalancer
selector:
app: voting-app
ports:
- port: 5000
targetPort: 80k3s creates one svclb pod per node per LoadBalancer service:
kubectl get pods -n kube-system | grep svclb-votingsvclb-voting-app-xxx 1/1 Running k3s-cp-01
svclb-voting-app-xxx 1/1 Running k3s-worker-01
svclb-voting-app-xxx 1/1 Running k3s-worker-02Three svclb pods = three node IPs assigned to the service.
Port Concepts — NodePort
Three separate port numbers, all can be different:
User hits: 172.16.77.10:31000 ← nodePort (on the node)
↓
Service: ClusterIP:80 ← port (service's own port)
↓
Pod receives: 10.42.2.x:80 ← targetPort (container port)ports:
- targetPort: 80 # 1. what the container listens on
port: 80 # 2. the service's own port
nodePort: 31000 # 3. external port on every node (30000-32767)Deploying the App
Five components, three service types:
| Component | Image | Service Type | Port |
|---|---|---|---|
| voting-app | dockersamples/examplevotingapp_vote:before | LoadBalancer | 5000→80 |
| result-app | dockersamples/examplevotingapp_result:before | LoadBalancer | 5001→80 |
| redis | redis | ClusterIP | 6379 |
| db | postgres:9.4 | ClusterIP | 5432 |
| worker | dockersamples/examplevotingapp_worker:latest | None | — |
Worker has no service — it only reads from redis and writes to db, never receives traffic.
Step 1 — Find the Ports
Before writing any manifests, pull the images and inspect them to find the exposed ports:
docker pull dockersamples/examplevotingapp_vote:before
docker inspect dockersamples/examplevotingapp_vote:before | python3 -m json.tool | grep -A5 ExposedPorts"ExposedPorts": {
"80/tcp": {}
}docker inspect redis:latest | python3 -m json.tool | grep -A5 ExposedPorts"ExposedPorts": {
"6379/tcp": {}
}docker inspect postgres:9.4 | python3 -m json.tool | grep -A5 ExposedPorts"ExposedPorts": {
"5432/tcp": {}
}
Step 2 — Create Namespace
kubectl create namespace voting
kubectl config set-context --current --namespace=votingStep 3 — Generate Deployments with dry-run
Use --dry-run=client -o yaml to generate base manifests:
kubectl create deployment redis --image=redis --replicas=1 --dry-run=client -o yaml > redis-deployment.yaml
kubectl create deployment db --image=postgres:9.4 --replicas=1 --dry-run=client -o yaml > db-deployment.yaml
kubectl create deployment voting-app --image=dockersamples/examplevotingapp_vote:before --replicas=1 --dry-run=client -o yaml > voting-app-deployment.yaml
kubectl create deployment result-app --image=dockersamples/examplevotingapp_result:before --replicas=1 --dry-run=client -o yaml > result-app-deployment.yaml
kubectl create deployment worker --image=dockersamples/examplevotingapp_worker:latest --replicas=1 --dry-run=client -o yaml > worker-deployment.yamlPostgreSQL needs credentials so edit db-deployment.yaml and add env vars under the container:
env:
- name: POSTGRES_PASSWORD
value: "postgres"
- name: POSTGRES_USER
value: "postgres"

Step 4 — Generate Services
# ClusterIP — internal only, no external access
kubectl create service clusterip redis --tcp=6379:6379 --dry-run=client -o yaml > redis-service.yaml
kubectl create service clusterip db --tcp=5432:5432 --dry-run=client -o yaml > db-service.yaml
# LoadBalancer — external access, k3s assigns all node IPs
kubectl create service loadbalancer voting-app --tcp=5000:80 --dry-run=client -o yaml > voting-app-service.yaml
kubectl create service loadbalancer result-app --tcp=5001:80 --dry-run=client -o yaml > result-app-service.yamlPort format is external:container — 5000:80 means port 5000 on the service, forwarding to port 80 on the pod.
We use ports 5000 and 5001 instead of 80 because Traefik already holds port 80 on all nodes as a host port.
Step 5 — Apply in Order
kubectl apply -f redis-deployment.yaml
kubectl apply -f redis-service.yaml
kubectl apply -f db-deployment.yaml
kubectl apply -f db-service.yaml
kubectl apply -f worker-deployment.yaml
kubectl apply -f voting-app-deployment.yaml
kubectl apply -f voting-app-service.yaml
kubectl apply -f result-app-deployment.yaml
kubectl apply -f result-app-service.yamlStep 6 — Verify
kubectl get deployments -n votingNAME READY UP-TO-DATE AVAILABLE
db 1/1 1 1
redis 1/1 1 1
result-app 1/1 1 1
voting-app 1/1 1 1
worker 1/1 1 1kubectl get svc -n votingNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
db ClusterIP 10.43.34.195 <none> 5432/TCP
redis ClusterIP 10.43.28.68 <none> 6379/TCP
result-app LoadBalancer 10.43.206.178 172.16.77.10,172.16.77.11,172.16.77.12 5001:30223/TCP
voting-app LoadBalancer 10.43.229.12 172.16.77.10,172.16.77.11,172.16.77.12 5000:31712/TCP

Access:
- Voting app:
http://172.16.77.10:5000 - Results:
http://172.16.77.10:5001

Service Type Summary
| Type | Accessible from | Use case |
|---|---|---|
| ClusterIP | Inside cluster only | Internal pod-to-pod — redis, db, internal APIs |
| NodePort | Node IPs on high port | Dev/testing, on-prem without LB controller |
| LoadBalancer | External IP/DNS | Production external traffic, user-facing apps |
Deleting a Service
Delete just the service without touching the deployment:
kubectl delete svc voting-app -n votingk3s automatically removes the svclb pods. Reapply when needed:
kubectl apply -f voting-app-service.yaml

