Problem Description
After a rolling update of a Deployment in a Kubernetes cluster, the NGINX Ingress controller started returning 502 Bad Gateway and 504 Gateway Timeout for the affected host. The HTTP response body was empty, and the NGINX error log contained entries such as:
2024/07/31 12:45:12 [error] 12#12: *12345 connect() failed (111: Connection refused) while connecting to upstream, client: 10.2.1.45, server: example.com, request: "GET /api/v1/resource HTTP/1.1", upstream: "http://my-service.default.svc.cluster.local:8080/api/v1/resource", host: "example.com"
2024/07/31 12:45:12 [error] 12#12: *12346 host not found in upstream "my-service.default.svc.cluster.local", client: 10.2.1.45, server: example.com, request: "GET /api/v1/resource HTTP/1.1", upstream: "http://my-service.default.svc.cluster.local:8080/api/v1/resource", host: "example.com"
2024/07/31 12:45:13 [error] 12#12: *12347 upstream prematurely closed connection while reading response header from upstream, client: 10.2.1.45, server: example.com, request: "GET /api/v1/resource HTTP/1.1", upstream: "http://my-service.default.svc.cluster.local:8080/api/v1/resource", host: "example.com"
Impact:
- External clients receive 502/504 errors.
- Service-level agreement (SLA) violations.
- Increased alert noise on
nginx_ingress_controller_upstream_response_time_secondsandnginx_ingress_controller_requestsmetrics.
Root Cause Analysis
The failure stemmed from a combination of three common Kubernetes‑NGINX integration pitfalls:
- Service selector mismatch after Deployment change – The new Deployment used a different
applabel (app: my-service-v2) while the existingServicestill selectedapp: my-service. Consequently the Service resolved to an empty endpoint list, producing the log “no upstreams are available” described in the NGINX upstream troubleshooting guide. - Kubernetes DNS outage – A
corednspod crash coincided with the rollout, causing temporary name resolution failures formy-service.default.svc.cluster.local. This manifested as “host not found in upstream” (see the GitHub issue #92758). - Trailing slash in
proxy_passURL – A ConfigMap change introduced a trailing slash (proxy_pass http://my-service.default.svc.cluster.local/;). According to the NGINX proxy_pass documentation, this causes NGINX to replace the entire request URI, leading to mismatched paths and “upstream sent unexpected response while reading response header”.
Because NGINX resolves upstream hosts at request time, any of the above conditions results in the observed 502/504 errors.
Investigation and Debugging
Step‑by‑step diagnostics that reproduced the issue:
- Inspect NGINX error logs for the failing request.
- Verify Service endpoints:
kubectl get svc my-service -n default -o jsonpath='{.spec.selector}'
kubectl get endpoints my-service -n default
Output showed an empty endpoint list:
NAME ENDPOINTS AGE
my-service <none> 12m
kubectl get deploy my-service -n default -o yaml | grep -A3 selector
kubectl get pods -l app=my-service -n default
The new pods carried app: my-service-v2, confirming the selector mismatch (similar to the incident “Service selector was mismatched, causing NGINX to resolve the Service to an empty endpoint list”).
kubectl exec -n ingress-nginx $(kubectl get pod -l app.kubernetes.io/name=ingress-nginx -n ingress-nginx -o name | head -n1) -- nslookup my-service.default.svc.cluster.local
The response was “server can't find my-service.default.svc.cluster.local: NXDOMAIN”, matching the “host not found in upstream” error.
kubectl -n ingress-nginx get configmap nginx-configuration -o yaml | grep proxy_pass
Found:
proxy_pass: "http://my-service.default.svc.cluster.local/";
The trailing slash was the cause of the path‑rewriting issue.
Resolution
The fix required three coordinated changes:
1. Align Service selector with Deployment labels
Before (service.yaml):
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
spec:
selector:
app: my-service # <-- outdated selector
ports:
- protocol: TCP
port: 8080
targetPort: http
After (updated selector):
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: default
spec:
selector:
app: my-service-v2 # <-- matches new pods
ports:
- protocol: TCP
port: 8080
targetPort: http
2. Restore CoreDNS health
Restart the failing CoreDNS pod and ensure the deployment has the desired replica count:
kubectl -n kube-system rollout restart deployment coredns
kubectl -n kube-system get pods -l k8s-app=kube-dns
After the rollout, nslookup from the Ingress pod succeeded.
3. Remove trailing slash from proxy_pass
Before (nginx‑configmap.yaml):
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-configuration
namespace: ingress-nginx
data:
proxy-pass: "http://my-service.default.svc.cluster.local/";
After (no trailing slash, per NGINX Docs – Configuring NGINX as a reverse proxy):
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-configuration
namespace: ingress-nginx
data:
proxy-pass: "http://my-service.default.svc.cluster.local";
Apply the updated ConfigMap and reload the Ingress controller:
kubectl apply -f nginx-configmap.yaml
kubectl -n ingress-nginx rollout restart deployment ingress-nginx-controller
Verification
Validate that the upstream is now reachable and the errors disappear:
- Check Service endpoints:
kubectl get endpoints my-service -n default
Expected output:
NAME ENDPOINTS AGE
my-service 10.244.1.12:8080,10.244.2.8:8080 2m
kubectl exec -n ingress-nginx $(kubectl get pod -l app.kubernetes.io/name=ingress-nginx -n ingress-nginx -o name | head -n1) -- nslookup my-service.default.svc.cluster.local
Should return the IPs shown above.
curl -I https://example.com/api/v1/resource
Expected HTTP status: 200 OK and no 502/504.
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx --tail=20 | grep -i "upstream"
No lines containing “connect() failed”, “host not found”, or “upstream prematurely closed”.
Operational Experience
- Misleading symptom: The initial 502 error suggested the backend was down, but the root cause was a label mismatch – a classic “service selector drift” after a rollout.
- DNS timing: A brief CoreDNS outage coincided with the deployment, amplifying the problem. In production, a rolling update should be coordinated with a health check on the
kube-dnsdeployment. - ConfigMap pitfalls: Adding a trailing slash in
proxy_passsilently altered request URIs, leading to backend 404s that appeared as upstream errors. Always validate ConfigMap syntax withnginx -tinside a temporary pod. - Readiness probes: Pods that failed their readiness probe were excluded from the Service endpoints, which prevented NGINX from routing to them. Ensure probes reflect actual service readiness.
Best Practices and Prevention
- Use a single source of truth for label values (e.g., Helm chart
.Values.labels.app) to avoid selector drift. - Enable Service topology aware routing (
topologyKeys) to reduce cross‑zone latency and limit the impact of a single pod failure. - Monitor
corednshealth viakube-system/corednspod restarts and exposecoredns_upmetrics to alert on DNS outages. - Validate NGINX configuration changes with a pre‑deployment test pod:
kubectl run nginx-test --image=nginx:stable-alpine --restart=Never --command -- sh -c "nginx -t && cat /etc/nginx/nginx.conf"
nginx.ingress.kubernetes.io/proxy-connect-timeout and nginx.ingress.kubernetes.io/proxy-read-timeout annotations to surface upstream timeouts early.kubectl wait --for=condition=available on Services before triggering a rollout that updates the Ingress.Related Topic Hub: Distributed Systems Troubleshooting Hub
FAQ
- Why does NGINX return 502 only after a Deployment rollout?
Because the new pods may carry different labels, causing the Service selector to match zero endpoints. NGINX then has no upstream to route to, producing “no upstreams are available”. - How can I tell if a DNS issue is causing “host not found in upstream”?
Run annslookupordigfrom the Ingress pod for the Service name. If it fails, check CoreDNS pod status and logs. - Does a trailing slash in
proxy_passalways cause path mismatches?
Yes. According to the NGINX proxy_pass documentation, a trailing slash causes NGINX to replace the full request URI, which can lead to 404s or “upstream sent unexpected response”. Remove the slash unless intentional path rewriting is required. - What alert thresholds should I set for upstream errors?
Alert on a spike ofnginx_ingress_controller_upstream_response_time_seconds> 5s or a rate ofnginx_ingress_controller_requests{status=5xx}exceeding 1% of total traffic for 5 minutes. - Can I use a headless Service with NGINX Ingress?
Yes, but you must setserviceNameto the headless Service and ensure the Ingress controller’sresolveannotation is enabled (nginx.ingress.kubernetes.io/upstream-vhost) so that DNS SRV records are resolved per request.