Blog · May 27, 2025 · Updated on September 7, 2026 · 11 min read

Kubernetes Service Types Explained

View down a busy supermarket aisle with category signs hanging above the shelves
Photo: BI ravencrow / Pexels

A Kubernetes Service is an object that gives a group of Pods a stable IP address and a DNS name and distributes incoming requests across those Pods. Pods come and go, and their IP addresses change with every restart. The Service stays put and uses labels to find whichever Pods are running right now. The four types ClusterIP, NodePort, LoadBalancer and ExternalName differ in one question only: where the Service can be reached from.

Anyone who has ever hard-wired a frontend to the IP address of a backend Pod knows why the Service exists. After the next rollout the address points to nothing, and the troubleshooting starts in the wrong place. The Service is the constant that removes this problem for good.

I run Kubernetes clusters in production and wrote the Kubernetes Practical Guide published by Rheinwerk (2024). The Service has its own section in chapter 3 of that book. What follows is the condensed version, focused on the points that raise the most questions in my projects. You can find all my Kubernetes articles collected on the Kubernetes page.

Why Pods need a stable address

Kubernetes gives every Pod its own IP address, and every Pod can reach every other Pod in the cluster directly, without NAT. The address you see inside the Pod is the same one other Pods use to talk to it. That sounds as if your frontend could simply call the backend by its IP address.

Technically it can. In practice it is a mistake. A Pod in Kubernetes is short-lived: the Deployment replaces it when it stops responding, autoscaling creates five of them in the morning and two at night, and a rollout of a new version replaces all of them one after another. Every new Pod gets a new IP address. So you need an entrance that stays in place while everything behind it changes.

That is exactly what the Service does, and it does it with three jobs. It knows all the Pods that belong to it and keeps that list current (service discovery). It spreads requests across the Pods that are available (load balancing). And it offers a fixed virtual IP address and a DNS name that other applications can rely on. On every node, the kube-proxy component makes sure that packets sent to that virtual address end up at one of the matching Pods.

Kubernetes service discovery: selector, labels and endpoints

The Service does not find its Pods by name but by labels. Under spec.selector you declare which labels a Pod has to carry for the Service to include it. Kubernetes continuously compares that selection with the running Pods and maintains a list of endpoints from it, meaning Pod IP plus port. Only Pods whose readiness probe succeeds make it onto that list.

The following manifest shows a Deployment with two Nginx Pods and the matching Service. The crucial part is that the label app: nginx in the Pod template and in the Service selector are identical.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80

port is the port the Service listens on, targetPort is the port inside the container. They may differ, and you can define several named ports, for example http and https. Without a type field you get a ClusterIP Service.

The most common mistake with Services is a selector that matches no label at all: the Service exists, it has an IP address, and you still get connection refused. A look at the endpoints reveals this immediately.

kubectl get service nginx-service
kubectl get endpoints nginx-service
kubectl describe service nginx-service

If the endpoints show <none>, either the selector does not match the Pod labels or no Pod is ready. How labels and selectors work together in detail is covered in Kubernetes Labels and Annotations Explained.

The Kubernetes Service types compared

All Service types build on each other. A NodePort is a ClusterIP Service with an extra port on every node, a LoadBalancer is a NodePort with an extra external IP address. Only ExternalName is the odd one out, because it does not point to any Pods.

Type Reachable from Typical use
ClusterIP inside the cluster only communication between applications, target for an Ingress
NodePort any node IP on a fixed port non-HTTP protocols, test clusters, simple setups
LoadBalancer the external IP address of a load balancer entry point for an Ingress controller, TCP services exposed externally
ExternalName inside the cluster, but points outside database or API outside the cluster under an internal name
Headless (not a type of its own, clusterIP: None) inside the cluster, without its own IP address StatefulSets, when clients need to address individual Pods

ClusterIP: the default inside the cluster

When someone talks about a Kubernetes Service, they almost always mean ClusterIP. The Service gets a virtual IP address from the cluster's service network and can only be reached from inside. For communication between your own applications that is exactly right, because none of it should be visible from outside. You can pin the address with clusterIP, but it has to lie within the service network and be free. In practice you rarely need that, because everyone goes through the DNS name anyway.

NodePort: one port on every node

With type: NodePort, Kubernetes opens the same port on every node in the cluster, by default from the range 30000 to 32767, and forwards everything arriving there to the Service. You then reach your application via the IP address of any node plus that port. You can pin the port with nodePort so it stays the same every time the Service is recreated.

apiVersion: v1
kind: Service
metadata:
  name: nginx-nodeport
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30080

NodePort works on layer 4: it sees packets and ports, not URLs or hostnames. For protocols beyond HTTP, such as a database or a message broker, it is the right choice. For web applications I only use it for testing, because an Ingress gives you far more control there.

LoadBalancer: an external IP address from your provider

A Kubernetes Service of type LoadBalancer asks the environment for an external load balancer with its own IP address. In a cloud, the provider's cloud controller manager takes care of that: it creates the load balancer, attaches the nodes behind it and writes the address into status.loadBalancer. In my clusters on Hetzner Cloud this is how a Hetzner load balancer appears, without me creating it by hand.

apiVersion: v1
kind: Service
metadata:
  name: nginx-loadbalancer
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80

Worth knowing: every LoadBalancer Service usually costs money at the provider and consumes its own IP address. That is why my clusters have exactly one Service of this type, sitting in front of the Ingress controller, and all web applications run behind it through Ingress rules.

ExternalName: a DNS alias pointing outside

ExternalName has no selector and no Pods. The Service is just a CNAME to an external hostname. Your application talks to database, and Kubernetes resolves that to the hostname of a database running outside the cluster. The appeal is interchangeability: in production the name points to the managed database, in the development environment you replace the ExternalName with a ClusterIP Service backed by a database Pod. The application code stays the same.

apiVersion: v1
kind: Service
metadata:
  name: database
spec:
  type: ExternalName
  externalName: db.example.com

Kubernetes Service DNS: how Pods find the Service

Every Service automatically gets a DNS record, served by the cluster DNS (CoreDNS in current clusters). The pattern is always the same: <service>.<namespace>.svc.cluster.local. A Service nginx-service in the namespace default is therefore called nginx-service.default.svc.cluster.local.

Inside the same namespace the short name nginx-service is enough, because the DNS configuration in the Pod adds its own namespace as a search domain. From another namespace you need at least nginx-service.default. I recommend always writing the name with the namespace in configuration files. It costs nothing and spares you the surprise when an application moves to a different namespace later on.

You can test this with a helper Pod inside the cluster:

kubectl run test --rm -it --image=busybox -- sh
wget -qO- nginx-service
wget -qO- nginx-service.default.svc.cluster.local

Both calls return the Nginx welcome page. For a quick look from your own machine, without NodePort or LoadBalancer, kubectl port-forward service/nginx-service 8080:80 does the job, and the Service then answers on localhost:8080.

LoadBalancer without a cloud: what happens on-premises

The question I hear most often from mid-sized companies: what happens with type: LoadBalancer when there is no cloud provider behind the cluster? The honest answer: nothing, at first. The Service stays at <pending> under EXTERNAL-IP, because nobody in the cluster knows how to obtain a load balancer. The ClusterIP and NodePort functionality is still there, only the external address is missing.

For your own hardware or a server at a hosting company, MetalLB fills that gap. You give it a range of free IP addresses from your network, and it assigns one of them to every LoadBalancer Service and makes it reachable via ARP or BGP. Your cluster then behaves like it would in a cloud, except that you manage the addresses yourself. k3s ships a simple variant called ServiceLB that uses the node IPs for LoadBalancer Services. That is fine for a single test system; for production with several nodes, MetalLB or a load balancer from your hosting provider is the more robust choice.

Whichever route you take: the LoadBalancer Service is the entrance on layer 4. As soon as you want to control hostnames, paths or TLS certificates, you need an Ingress behind it. How the two fit together and which Ingress controller suits which situation is covered in Kubernetes Ingress and Ingress Controllers.

Frequently asked questions

What is the difference between a Service and an Ingress?

The Service gives Pods a stable address and distributes requests on layer 4, meaning by IP address and port. The Ingress works on layer 7 and decides, based on the hostname and path of an HTTP request, which Service it goes to. An Ingress always needs a Service as its target; a Service does not need an Ingress.

Why is my LoadBalancer Service stuck in pending?

Because there is no cloud controller manager and no MetalLB in the cluster that could assign an external IP address. In a cloud, the integration with the provider is usually missing; on-premises, MetalLB or a comparable component is missing. The Service itself still works as ClusterIP and NodePort.

Is NodePort a good idea in production?

Not for HTTP applications. The port sits in an unusual range, every node has to expose it, and you have no control over hostnames or TLS. For individual TCP services behind your own firewall, or as the foundation for an external load balancer, NodePort is a legitimate and simple option.

How do I reach a Kubernetes Service from another namespace?

Through the DNS name including the namespace, so <service>.<namespace> or the full form <service>.<namespace>.svc.cluster.local. The short name alone only works within the same namespace. Whether the access is allowed is not decided by the Service but, if configured, by a NetworkPolicy.

Can I create a Service without a selector?

Yes. Kubernetes then does not maintain the targets automatically, and you create them yourself, for example with the IP address of a system outside the cluster. The current object for that is EndpointSlice; the older Endpoints object still exists, but new setups write EndpointSlices. That is the alternative to ExternalName when you have an IP address instead of a hostname or need port mapping.

Where to go from here

The Service is the bridge between your Pods and everything that wants to reach them. Inside the cluster, ClusterIP with DNS is enough; toward the outside, the path usually leads through a single LoadBalancer Service in front of an Ingress controller. How you then steer HTTP traffic with hostnames, paths and certificates is described in Kubernetes Ingress and Ingress Controllers. If you want to revisit the fundamentals first, Kubernetes Pod: What Is a Pod? is the right starting point.

My suggestion for today: create the Deployment and the Service from above, check with kubectl get endpoints that both Pods are listed, and then change the label in the selector as a test. You will see right away how the endpoints disappear and the application stops answering. The complete reference for every field is in the Kubernetes documentation on Services, and the naming rules are in the documentation on DNS for Services and Pods.

With all the examples, from the first ClusterIP through NodePort to the interplay with Ingress, this is covered in chapter 3 of my Kubernetes Practical Guide (Rheinwerk Computing).

Kevin Welter

Kevin Welter

Developer, IT architect, author of technical books (Kubernetes, cloud infrastructures) and speaker. Runs his business with an AI workforce of eight AI employees and shows solo business owners in his community how to hire their first AI employee.

More about AI employees

Kubernetes from the basics to a production-ready cluster

Get the book