NGINX Error Guide: 'no resolver defined to resolve' — Fix Dynamic DNS Upstreams
Fix 'no resolver defined to resolve' in NGINX: add a resolver for variable proxy_pass targets, set the DNS server and TTL, and avoid stale IPs.
- #nginx
- #web-server
- #troubleshooting
- #errors
Stuck on this NGINX error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
NGINX raises this at request time whenever it must resolve a hostname at runtime but no resolver directive is configured. It typically appears when proxy_pass (or grpc_pass, uwsgi_pass, an auth_jwt_key_request, an njs fetch, etc.) points at a hostname held in a variable:
2026/07/09 09:41:53 [error] 992#992: *5027 no resolver defined to resolve api.internal.example.com, client: 10.0.0.7, server: gateway.example.com, request: "GET /v1/orders HTTP/1.1", host: "gateway.example.com"
The client sees a 502 Bad Gateway. The key trigger is dynamic resolution: when a hostname appears in a proxy_pass as a literal, NGINX resolves it once at startup using the system resolver. But when the target is built from a variable, NGINX defers resolution to request time and needs an explicit resolver to do it.
Symptoms
502 Bad Gatewayon any route whoseproxy_passuses a variable containing a hostname.- The error log shows
no resolver defined to resolve <hostname>— with the exact hostname it failed on. - Static
proxy_pass http://api.internal.example.com;locations work fine; only the variable-based ones break. - The problem appears after refactoring a config to use
set $backend ...;or amap, or after enabling dynamic upstreams for service discovery.
Common Root Causes
- Variable
proxy_passwithout a resolver — e.g.set $upstream "api.internal"; proxy_pass http://$upstream;with noresolverin scope. - Runtime DNS features in use —
grpc_pass,uwsgi_pass,auth_requestto a hostname,njsfetch, or OpenID Connect key requests, all of which resolve at request time. - Resolver defined in the wrong scope — placed in a
serverblock that the failinglocationdoesn’t inherit, or missing from thestreamcontext entirely. - Service-discovery patterns — pointing at a Kubernetes service DNS name or Consul name that must be re-resolved as pods change.
Diagnostic Workflow
Find the offending location — it will use a variable in proxy_pass:
grep -rnE 'proxy_pass\s+https?://\$' /etc/nginx/
grep 'no resolver defined' /var/log/nginx/error.log | tail
Confirm the host can be resolved from the NGINX box and identify a DNS server to point NGINX at:
# What resolver does the OS use? (do NOT blindly reuse 127.0.0.53 stub in containers)
cat /etc/resolv.conf
dig +short api.internal.example.com @10.0.0.2
Add a resolver directive in the same context as the dynamic proxy_pass, and use a variable target so runtime resolution is honored:
http {
# Point at a real DNS server reachable from NGINX. valid= sets re-resolution TTL.
resolver 10.0.0.2 valid=30s ipv6=off;
resolver_timeout 5s;
server {
location /v1/ {
set $backend "api.internal.example.com";
proxy_pass http://$backend:8080; # variable target -> resolved at request time
proxy_set_header Host $backend;
}
}
}
For the stream context, the resolver must be inside the stream {} block — it does not inherit from http. Validate and reload:
nginx -t && nginx -s reload
Example Root Cause Analysis
A platform team migrated an internal API gateway to Kubernetes and changed proxy_pass http://orders-svc; to set $svc "orders-svc.prod.svc.cluster.local"; proxy_pass http://$svc; so NGINX would re-resolve the ClusterIP as the service moved. Immediately, every /v1/orders request returned 502 with no resolver defined to resolve orders-svc.prod.svc.cluster.local.
The literal-to-variable change was the trigger: with a literal hostname NGINX resolved once at startup, but the variable form defers resolution to request time and demands a resolver. They added resolver <kube-dns-clusterIP> valid=10s ipv6=off; at the http level, pointing at the cluster’s CoreDNS service, with a short valid= TTL so NGINX would pick up new endpoints quickly. The 502s cleared. They also set proxy_set_header Host $svc; because using a variable target drops the implicit Host header that a literal proxy_pass would have set.
Prevention Best Practices
- Any time a
proxy_pass/grpc_passtarget comes from a variable, add aresolverin the same context (rememberstream {}needs its own). - Point
resolverat a real, reachable DNS server — in containers, avoid the127.0.0.53systemd stub, which usually isn’t present inside the container. - Set a sensible
valid=TTL to control how often NGINX re-resolves; short TTLs track fast-changing service discovery, longer TTLs reduce DNS load. - Add
resolver_timeoutand consideripv6=offif your upstreams are IPv4-only, to avoid slow AAAA lookups. - When using a variable target, set
proxy_set_header Hostexplicitly — the variable form does not infer the Host header the way a literalproxy_passdoes. - Keep static upstreams static: if a hostname never changes, a literal
proxy_pass(resolved at startup) is simpler and needs no resolver.
Quick Command Reference
# Find dynamic proxy_pass targets that need a resolver
grep -rnE 'proxy_pass\s+https?://\$' /etc/nginx/
# See the exact hostname that failed
grep 'no resolver defined' /var/log/nginx/error.log | tail
# Verify a DNS server can resolve the upstream
dig +short UPSTREAM_HOST @DNS_SERVER_IP
# Validate and reload after adding resolver
nginx -t && nginx -s reload
Conclusion
no resolver defined to resolve means NGINX needs to look up a hostname at request time — because the target lives in a variable or a runtime feature like grpc_pass/auth_request — but you never told it which DNS server to use. Add a resolver in the same context as the dynamic directive (including a separate one inside stream {}), point it at a reachable DNS server with a sensible valid= TTL, and set the Host header explicitly for variable targets. That turns brittle startup-only resolution into working, TTL-aware dynamic upstreams.
Fixed it? Get 500 NGINX & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.