# ndots : The Hidden DNS Logic Behind resolving Kubernetes Service Names

Ever wondered how Kubernetes resolves a service name like `my-service` without any domain attached?  
In this blog, we’ll dive into the DNS concept of `ndots`, understand how it works, and see how it plays a crucial role in Kubernetes DNS resolution.

Before we dive deeper, let’s take a step back and understand how a basic DNS query and resolution actually work

![](https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExNGEweGZhOHNlaXZ2cmFrb3RidHAwNHR1M2o2anZsbGFjeGtwdmd6ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Kehn32T6zE1vq6rxYX/giphy.gif align="center")

---

# Working of a DNS Query

so for example - let’s understand what happens when you enter `blogs.kratik.dev` in your browser

(I hope somewhere someone is actually entering this to read my blogs 🫠 )

Your browser tries to resolve IP of the domain in following order

1. check if domain exists in the `/etc/hosts` file
    
2. If the domain is not found there, the OS sends the request to the DNS servers configured on the device (from Wi-Fi, Ethernet, mobile data, or VPN).
    

> On Linux, these DNS server settings are exposed through `/etc/resolv.conf`;  
> on other systems, they are managed internally, but the behaviour is the same.

## what is /etc/hosts file ?

> `/etc/hosts` is a **local, static hostname-to-IP mapping file** used by the operating system **before DNS is consulted**. It allows you to manually define how hostnames resolve **without querying any DNS server**.

in my machine, it looks something like this :

```apache
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost

# Added by Docker Desktop
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal

127.0.0.1 kratik.servers
# End of section
```

💡- now you also know how localhost IP resolution works :)

so let’s test this with a domain we have defined `kratik.awesome` - it does not exist

**let’s verify it first**

curl

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768986802183/8ec290db-5ba1-4641-84c8-467bce4203d5.png align="center")

browser

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768987011921/66e2fe77-ed57-4fe8-9a69-f7b84ff5adc0.png align="center")

okay, obviously it does not exist

(*I wish It did* )

![](https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdnNkcTA4OTdlYmd1M3VrYTh2eXM4NXF3MXZoYWF2cnU4cWhkcHZtbyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/o5IxfV1v8oU1vZUeZA/giphy.gif align="center")

Let’s add this domain in our `/etc/hosts` file

```apache
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost

# Added by Docker Desktop
# To allow the same kube context to work on the host and the container:
127.0.0.1 kubernetes.docker.internal

127.0.0.1 kratik.servers

127.0.0.1 kratik.awesome
# End of section
```

now let’s run a test server in a python quickly 😎

(basically this commands runs a python server which serves files from the specified directory)

```bash
mkdir -p /tmp/sample

cat <<EOF >/tmp/sample/index.html
<h1>Hello from DNS Blog!</h1>
EOF

python3 -m http.server 80 --directory /tmp/sample
```

Ok, let’s check browser again

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768989896262/6c23c12c-be4f-4a6b-9fa0-1fe7167f843a.png align="center")

now you can see the above webpage, because :

1. browser requested the IP of domain `kratik.awesome`
    
2. `/etc/hosts` had an entry for above domain so replied with the IP instantly which was `127.0.0.1`
    
3. we already running a Python HTTP server on port 80, so this server responded with our created HTML file.
    

I hope that clear the basics for `/etc/hosts` file, let’s move next to `resolv.conf` file

## what is /etc/resolv.conf file

> `/etc/resolv.conf` is a **system configuration file** that tells the operating system **which DNS servers to use** when resolving domain names.

it look something like this on a linux machine (this is an AWS EC2 machine btw)

```nginx
# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients directly to
# all known uplink DNS servers. This file lists all configured search domains.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.

nameserver 172.22.0.2
search ap-south-1.compute.internal
```

basic explanation of the fields in resolver files are:

1. `nameserver` - **server to query for DNS lookups**, you can define more servers by adding one more nameserver in same format in a new line.
    
2. `search` - this option tells the system **which domain names to automatically append** when you try to resolve a short (unqualified) hostname.
    
3. `option` - (it’s not present in the above file) - this field is used to **configure the behaviour of the DNS lookups**, for example - how long it waits, how many times it retries, and when it treats a name as fully qualified - we will understand it better further.
    
    available options are **ndots** & **timeout** & **attempts**
    

now let’s move deeper and check a resolver file in Kubernetes environment

# /etc/resolv.conf file in Kubernetes Pods

If you see the same file in your pods in a Kubernetes cluster, it will look something like this

```apache
search kube-system.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
```

This is where DNS resolution gets interesting: **search** domains and `ndots`

![](https://i.giphy.com/MP1kygLQzjCve.webp align="center")

Let’s understand it one by one

## search

```nginx
search kube-system.svc.cluster.local svc.cluster.local cluster.local
```

**What it does :** If a hostname is not fully qualified, the resolver tries appending each of these domains in order until one resolves.

## ndots

```nginx
options ndots:5
```

so this actually counts the dots(.) in our DNS query, for example - **api.github.com** has 2 dots and **my-service.my-namespace.svc.cluster.local** has 4 dots

so basically it means, if a domain does not contain dots more than or equal to 5, ***it will not be considered as a fully qualified domain name*** and then all the domains listed in \`search\` will be appended in the DNS lookup one by one.

Let’s see this in action!

*I will do a DNS query and also show you the logs of the dns server in k8s.*

## **Setup of the k8s cluster for this test**

We have a Kubernetes cluster with **two namespaces**:

* `my-namespace`
    
    * also create a nginx service here named `my-service`
        
* `my-other-namespace`
    

Each namespace has its own DNS search domain:

* `my-namespace.svc.cluster.local`
    
* `my-other-namespace.svc.cluster.local`
    

Kubernetes configures pods so that DNS lookups first try services in the **same namespace**, and then fall back to cluster-wide service discovery.

I will create [netshoot](https://github.com/nicolaka/netshoot) pods to do DNS lookup in each of these namespace.

## Case 1: Lookup from the same namespace (`my-namespace`)

### cat /etc/resolv.conf file in 1st namespace

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998514105/1f29ac4c-aa79-4dcf-b964-3107066732db.png align="center")

### Command

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998241581/fbf8d50c-b5c0-4e13-8546-8fd5c63c48b8.png align="center")

### CoreDNS logs

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998267828/5d8e74eb-98b9-4662-ae33-92a22da30f8f.png align="center")

```bash
"A IN my-service.my-namespace.svc.cluster.local." NOERROR
"AAAA IN my-service.my-namespace.svc.cluster.local." NOERROR
```

### What happened

* The pod’s resolver has this search list:
    
    ```bash
    search my-namespace.svc.cluster.local svc.cluster.local cluster.local
    ```
    
* Since `my-service` is a short name, the resolver **appends the first search domain**.
    
* The first attempt is:
    
    ```bash
    my-service.my-namespace.svc.cluster.local
    ```
    
* This service **exists**, so CoreDNS returns `NOERROR`.
    
* Resolution **stops immediately**.
    

✅ Result: `my-service` resolves successfully inside its own namespace.

---

## Case 2: Lookup from another namespace (`my-other-namespace`)

### cat /etc/resolv.conf file in 2nd namespace

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998473352/8465cc16-557b-49a7-97a3-44d18aa4c19f.png align="center")

### Command

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998302114/cbeeb89a-4a2c-448e-bd40-80b5ba574b19.png align="center")

### CoreDNS logs (in order)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768998313070/6a80cf69-5cee-4282-9b06-f3d3bbb31f95.png align="center")

```bash
"A IN my-service.my-other-namespace.svc.cluster.local." NXDOMAIN
"A IN my-service.svc.cluster.local." NXDOMAIN
"A IN my-service.cluster.local." NXDOMAIN
"A IN my-service." NXDOMAIN
```

### What happened

From `my-other-namespace`, the resolver search list is:

```bash
search my-other-namespace.svc.cluster.local svc.cluster.local cluster.local
```

The resolver tries **each option one by one**:

1. `my-service.my-other-namespace.svc.cluster.local`  
    **Service does not exist** → `NXDOMAIN`
    
2. `my-service.svc.cluster.local`  
    **No service with that name across namespaces** → `NXDOMAIN`
    
3. `my-service.cluster.local`  
    **Not a valid service zone** → `NXDOMAIN`
    
4. `my-service` (as-is)  
    **No global DNS record** → `NXDOMAIN`
    

After all attempts fail, the lookup fails.

**Result**: `my-service` does **not** resolve from another namespace.

additionally, as soon as I add the namespace name in my lookup, it works! even in the other namespace. why? because now one of the search domain led to a valid fully qualified domain name.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768999391512/c9286f5f-fbaf-4f72-ac9d-d1f389050ba5.png align="center")

CoreDNS logs

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1768999485437/7866973e-efa2-4c10-b9fd-e58ba6278a21.png align="center")

---

In this blog, we walked through how DNS resolution works at a basic level and then explored how Kubernetes builds on top of it using search domains and `ndots`.

By understanding how these pieces fit together, you can better debug DNS issues, avoid unnecessary lookups, and design more predictable service communication inside your cluster.

💡 - Default value of **ndots** in Linux (glibc) is 1 and in k8s environments it’s 5

---

## Bonus Tip

### How to skip adding search domains to your DNS query?

You can do this by **adding a dot (**`.`) at the end of the hostname.

for example :

```bash
nslookup my-service.
curl my-service.
```

### What the trailing dot does

> A trailing dot tells the resolver:  
> **“This name is already fully qualified — do not append search domains.”**

### Where is the trailing dot useful?

Adding a trailing dot (`.`) is useful whenever you want **full control over DNS resolution** and want to avoid resolver-side surprises.

---

Hope you liked this knowledge byte.

Tune in for more blogs. Time to clear the year old drafts.

see you in the next one!

![](https://i.giphy.com/sDjIG2QtbXKta.webp align="center")
