When running Nginx as a reverse proxy in front of containerized microservices in Docker or Docker Compose, Nginx may crash during boot or enter a continuous restart loop with the following emergency error:
nginx: [emerg] host not found in upstream "api_backend" in /etc/nginx/conf.d/default.conf:12
In container process listings, the service displays an unstable lifecycle:
$ docker compose ps
NAME IMAGE COMMAND SERVICE STATUS PORTS
nginx-1 nginx:alpine "/docker-entrypoint.…" nginx Restarting (1) 3 seconds ago
api_backend-1 my-api:latest "gunicorn -b 0.0.0.0…" api_backend Up 10 seconds (healthy) 8000/tcp
This error halts edge traffic routing completely. In this guide, we examine the architectural differences between Nginx configuration-time DNS evaluation and runtime DNS resolution, detail Docker's embedded 127.0.0.11:53 resolver mechanics, and provide production-ready solutions using dynamic Nginx variables, Compose health synchronization, and CLI network diagnostics.
[!TIP] Proactive Gateway & DNS Verification When debugging Docker container routing, reverse proxy timeouts, or broken upstream networks, verify your external endpoints and headers instantly with our free DNS Lookup Tool, HTTP Header Checker, and Ping Test Tool.
The Core Failure Model: Static DNS vs. Runtime Resolution
To resolve this issue permanently, you must understand how Nginx processes upstream hostnames during initialization.
+--------------------------------------------+
| docker compose up |
+--------------------------------------------+
| |
(Spawns Nginx) | | (Spawns Backend)
v v
+-------------------+ +-------------------+
| Nginx Boot | | Backend Boot |
| Reads config | | Runs DB Migr. |
| Resolves Host | | App Initializing |
+-------------------+ +-------------------+
|
[ Static proxy_pass http://api:8000; ]
|
+-------------+-------------+
| |
[ DNS Succeeds ] [ DNS Fails ]
| |
v v
Nginx Listens :80 Nginx Container Dies
(Restart Loop Crash)
1. Static Configuration-Time Resolution
In standard Nginx configurations, upstream targets are declared statically:
location /api/ {
proxy_pass http://api_backend:8000;
}
When Nginx parses nginx.conf at startup:
- It executes a synchronous, one-time DNS lookup for
api_backendusing the system resolver in/etc/resolv.conf. - If the record resolves to an IPv4 address, Nginx pins that IP address permanently in memory for the lifetime of the master process.
- If DNS resolution fails (because the
api_backendcontainer has not registered its hostname with Docker's embedded DNS server yet), Nginx treats this as a fatal syntax/configuration failure and exits immediately with[emerg] host not found in upstream.
2. Docker Embedded DNS (127.0.0.11:53)
On user-defined bridge networks (which Docker Compose creates by default for every project stack), Docker runs an embedded DNS resolver at 127.0.0.11:53.
Each container's /etc/resolv.conf is populated with:
nameserver 127.0.0.11
options ndots:0
Because container IP addresses are dynamic ($172.18.0.2 \to 172.18.0.5$ on recreation), static Nginx host caching creates two major failure vectors:
- Startup Race Condition: Nginx starts before the backend container registers with
127.0.0.11, triggering an immediate container crash loop. - Stale IP Blackholing: If the backend container restarts and receives a new IP address, Nginx continues routing requests to the old cached IP, returning persistent
502 Bad Gatewayerrors until Nginx is restarted.
Solution 1: Dynamic Runtime DNS Resolution with Variables
The most robust architectural solution is forcing Nginx to resolve upstream service names dynamically at runtime instead of startup. This is achieved by assigning the upstream URL to an internal Nginx variable combined with the resolver directive.
Nginx Dynamic Configuration (/etc/nginx/conf.d/default.conf)
server {
listen 80;
server_name _;
# Direct Nginx to use Docker's embedded 127.0.0.11 DNS server
# valid=10s forces re-resolution every 10 seconds to track container IP changes
# ipv6=off avoids delays when Docker networks do not have dual-stack routing
resolver 127.0.0.11 valid=10s ipv6=off;
resolver_timeout 5s;
location /api/ {
# Setting the target in a variable forces dynamic runtime DNS resolution
set $upstream_backend http://api_backend:8000;
proxy_pass $upstream_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Socket and connect timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Why This Fixes the Crash
When proxy_pass references a variable ($upstream_backend):
- Nginx skips configuration-time host resolution during boot.
- Nginx starts successfully even if
api_backendis currently stopped or restarting. - Every request queries
127.0.0.11(cached for10s), automatically tracking backend IP changes across deployments.
Solution 2: Docker Compose Health Checks and Startup Ordering
If your architecture prefers static proxy_pass declarations without variable lookups, you must prevent the startup race condition by enforcing service health synchronization in docker-compose.yml.
By default, Compose's depends_on: [api_backend] only waits until the container is created, not until its internal HTTP process is ready.
Production docker-compose.yml with Health Check Synchronization
version: '3.8'
services:
nginx:
image: nginx:1.29-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
api_backend:
condition: service_healthy
networks:
- app_network
restart: unless-stopped
api_backend:
image: my-api:latest
expose:
- "8000"
networks:
- app_network
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://127.0.0.1:8000/health || exit 1"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
networks:
app_network:
driver: bridge
With condition: service_healthy:
- Docker Compose initializes
api_backend. - Compose polls the
/healthendpoint until it returns HTTP200. - Only after the backend registers with Docker's embedded DNS and passes the health check does Compose start Nginx.
Solution 3: Network Segregation & Multi-Network Audits
A frequent source of host not found in upstream is placing Nginx and the backend on mismatched networks.
Broken Configuration (Isolated Networks)
services:
nginx:
image: nginx:alpine
networks:
- frontend_net # <-- Nginx is only on frontend_net
api_backend:
image: my-api:latest
networks:
- backend_net # <-- Backend is isolated on backend_net
Because Docker's embedded DNS server 127.0.0.11 isolates service discovery per network bridge, Nginx cannot resolve api_backend.
Fixed Configuration (Shared Bridge Network)
services:
nginx:
image: nginx:alpine
networks:
- frontend_net
- backend_net # <-- Nginx joins both networks
api_backend:
image: my-api:latest
networks:
- backend_net
Step-by-Step CLI Diagnostic Hierarchy
Follow this diagnostic sequence to identify whether the breakdown is at the DNS, TCP, or HTTP application layer:
[ Step 1: Compose State ] -> [ Step 2: Docker DNS Query ] -> [ Step 3: TCP Port Check ] -> [ Step 4: HTTP 200 Probe ]
1. Test Service Name Resolution from Inside Nginx
Execute a DNS lookup against Docker's embedded DNS resolver:
# Check hostname resolution via getent
docker compose exec nginx getent hosts api_backend
# Query embedded DNS with nslookup
docker compose exec nginx nslookup api_backend 127.0.0.11
# Short query with dig
docker compose exec nginx dig +short @127.0.0.11 api_backend
Expected output:
172.20.0.5 api_backend
If these commands return NXDOMAIN or Host not found, verify that both containers are attached to the same network bridge:
docker network inspect <project_name>_app_network
2. Verify TCP Socket Reachability
Confirm that Nginx can establish a TCP handshake with the backend container on port 8000:
docker compose exec nginx nc -zvw3 api_backend 8000
Expected output:
api_backend (172.20.0.5:8000) open
If this returns Connection refused:
- Check what interface your backend application is binding to. If it binds to
127.0.0.1:8000, it will only accept requests from inside its own container. - Ensure your application binds to
0.0.0.0:8000(all interfaces).
# Verify backend listening sockets
docker compose exec api_backend ss -lntp
3. Verify Backend HTTP Response
Test the actual HTTP payload from inside the Nginx container:
docker compose exec nginx curl -v http://api_backend:8000/health
Expected output:
< HTTP/1.1 200 OK
< Content-Type: application/json
{"status":"healthy"}
Static Upstream vs. Dynamic Variable Resolver Matrix
| Characteristic | Static proxy_pass http://host:port; | Dynamic proxy_pass $var; + resolver |
|---|---|---|
| DNS Resolution Phase | Configuration load / container startup | Runtime (per request or TTL cache) |
| Resolver Directive Required | No | Yes (resolver 127.0.0.11;) |
| Behavior on Backend Crash | Crashes Nginx if backend is missing on boot | Nginx boots cleanly; returns 502 if down |
| Backend IP Regeneration | Pinned permanently; requires Nginx reload | Automatically updates based on valid=10s |
| IPv6 Fallback Suppression | OS resolver behavior | Explicit (ipv6=off) |
| Configuration Overhead | Minimal | Requires variable declaration |
| Recommended Environment | Static bare-metal / fixed IP topologies | Docker Compose, Kubernetes, Dynamic Cloud |
Edge Cases: IPv6 AAAA Delays & Orchestration Platforms
1. IPv6 AAAA Lookup Timeouts
By default, Nginx's resolver issues dual queries for both A (IPv4) and AAAA (IPv6) records. In IPv4-only Docker bridge networks, the missing AAAA response may introduce a 3–5 second latency penalty before falling back to IPv4. Always include ipv6=off in your resolver declaration:
resolver 127.0.0.11 valid=10s ipv6=off;
2. Migrating to Docker Swarm or Kubernetes
- Docker Swarm: In Swarm mode, the embedded DNS server remains at
127.0.0.11, but routing uses overlay Virtual IPs (VIPs). - Kubernetes: Kubernetes does not use
127.0.0.11. Upstream names must use ClusterIP FQDNs (e.g.,api-backend.production.svc.cluster.local) and the CoreDNS resolver (typically10.96.0.10orkube-dns.kube-system.svc.cluster.local).
Proactive Microservice Resilience with Pingzo
Preventing silent container routing failures and reverse proxy outages requires continuous multi-location synthetic monitoring. When container restarts corrupt internal DNS or reverse proxies fail to route traffic, Pingzo alerts your engineering team instantly.
+-----------------------------------------------------------------------------+
| Pingzo Global Edge Network |
+-----------------------------------------------------------------------------+
| |
[ Multi-Region HTTP/S Probes ] [ SSL & Domain Expiry Audits ]
| |
v v
+-----------------------------------------------------------------------------+
| Docker / Nginx Gateway & Microservices Infrastructure |
| (Instant detection of 502, 504, 500, or DNS Failures) |
+-----------------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------------+
| Multi-Channel Alerts via WhatsApp, Slack, & Telegram |
+-----------------------------------------------------------------------------+
Pingzo Monitoring Plans
| Feature | Free | Starter ($5/mo) | Pro ($12/mo) | Agency ($29/mo) |
|---|---|---|---|---|
| Check Frequency | 15 minutes | 5 minutes | 2 minutes | 1 minute |
| Monitors | 1 monitor | 10 monitors | Unlimited | Unlimited |
| Multi-Location Testing | Global | Global | Global | Global |
| SSL & DNS Audits | Basic | Advanced | Real-Time | Real-Time + Custom Root |
| Alert Channels | Email, Telegram, WhatsApp, Slack, Discord | All Channels + Multi-Recipient | Webhooks, SMS, White-Label Dashboards | |
| Multi-Tenant Management | No | No | No | Yes (Multi-Team) |
Deploy real-time synthetic monitoring and container health checks today with Pingzo.
Stop Finding Out About Outages from Angry Users
Get instant WhatsApp & Discord alerts the second your API, website, or server goes down. Setup in 30 seconds with 60-second checks.