What is a Headless Service?
Normally, a Kubernetes Service provides a single virtual IP (ClusterIP) and load balances traffic across pods.
But for a StatefulSet, we don’t want load balancing —
we need each pod to have its own stable DNS identity (e.g., mysql-0, mysql-1, etc.).
So we create a headless service, by setting:
clusterIP: None
This tells Kubernetes:
Don’t give me a virtual IP. Instead, create individual DNS records for each pod.
Example: MySQL StatefulSet with Headless Service
Headless Service
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # Headless service
selector:
app: mysql
ports:
- port: 3306
StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # Important: Must match the service name above
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: rootpass
How Kubernetes Resolves DNS for Headless Service
When the above StatefulSet and Service are deployed, Kubernetes automatically assigns stable DNS hostnames for each pod.
For example (assuming namespace = default):
-
mysql-0
- DNS Hostname:
mysql-0.mysql.default.svc.cluster.local - Resolves To: Pod IP of mysql-0
- DNS Hostname:
-
mysql-1
- DNS Hostname:
mysql-1.mysql.default.svc.cluster.local - Resolves To: Pod IP of mysql-1
- DNS Hostname:
-
mysql-2
- DNS Hostname:
mysql-2.mysql.default.svc.cluster.local - Resolves To: Pod IP of mysql-2
- DNS Hostname:
This means:
-
Each pod gets its own unique DNS name.
-
The service name (
mysql) is the subdomain. -
The pattern is:
<pod-name>.<service-name>.<namespace>.svc.cluster.local
How to Use It (Inside and Outside Cluster)
Inside the same Kubernetes namespace
If another pod (e.g., an app) wants to connect to MySQL:
-
To connect to the primary instance (e.g., mysql-0):
mysql -h mysql-0.mysql -u root -p -
Or specify a JDBC URL (for Java apps):
spring.datasource.url=jdbc:mysql://mysql-0.mysql:3306/mydb spring.datasource.username=root spring.datasource.password=rootpass
Kubernetes automatically resolves mysql-0.mysql → Pod IP of mysql-0.
Inside a different namespace
If your app runs in another namespace (say backend):
Then the fully qualified domain name (FQDN) must include the namespace:
mysql -h mysql-0.mysql.default.svc.cluster.local -u root -p
or in your connection string:
spring.datasource.url=jdbc:mysql://mysql-0.mysql.default.svc.cluster.local:3306/mydb
Connecting to any pod (for load balancing or replica reads)
You can use the Service name directly (mysql) to get a list of pod IPs.
For example:
mysql -h mysql -u root -p
Since it’s a headless service, this will resolve to multiple A records:
mysql.default.svc.cluster.local. -> [10.244.1.12, 10.244.2.9, 10.244.3.4]
Your client (e.g., MySQL driver) may pick one randomly or use them all (depending on client logic).
Common Use Cases
-
Single-master DB (like MySQL primary)
- DNS to Use:
mysql-0.mysql - Description: Connects to master pod
- DNS to Use:
-
Replica reads
- DNS to Use:
mysql-1.mysql,mysql-2.mysql - Description: Connect to replica pods individually
- DNS to Use:
-
Client-side load balancing
- DNS to Use:
mysql(headless service name) - Description: Resolves to all pods; client decides
- DNS to Use:
-
Cross-namespace access
- DNS to Use:
mysql-0.mysql.<namespace>.svc.cluster.local - Description: Use full FQDN
- DNS to Use:
Example with JDBC Connection Strings
-
Connect to a single MySQL pod
- Example JDBC URL:
jdbc:mysql://mysql-0.mysql:3306/mydb
- Example JDBC URL:
-
Connect to replicas manually
- Example JDBC URL:
jdbc:mysql://mysql-1.mysql:3306,mydb
- Example JDBC URL:
-
Connect to cluster via client-side load balancing (supported drivers)
- Example JDBC URL:
jdbc:mysql:loadbalance://mysql-0.mysql,mysql-1.mysql,mysql-2.mysql:3306/mydb
- Example JDBC URL:
-
Full qualified form (cross-namespace)
- Example JDBC URL:
jdbc:mysql://mysql-0.mysql.default.svc.cluster.local:3306/mydb
- Example JDBC URL:
Why the Headless Service Matters
Without clusterIP: None, all pods behind the Service share one IP — Kubernetes load balances requests randomly.
That’s fine for stateless apps, but disastrous for databases that need predictable pod addressing (e.g., master vs replica).
Headless services let StatefulSets expose individual pod endpoints to the rest of the cluster.
Quick DNS Demo
Run inside any pod:
kubectl exec -it some-pod -- nslookup mysql.default.svc.cluster.local
You’ll see multiple IPs (one per StatefulSet pod). And:
kubectl exec -it some-pod -- nslookup mysql-0.mysql.default.svc.cluster.local
You’ll get exactly one IP (that of mysql-0).