Back to blog
Guide March 28, 2026

How to Fix Nginx "host not found in upstream" in Docker and Docker Compose

PPingzo Infrastructure Team
Automate WhatsApp Alerts
Start Free ➔

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:

  1. It executes a synchronous, one-time DNS lookup for api_backend using the system resolver in /etc/resolv.conf.
  2. If the record resolves to an IPv4 address, Nginx pins that IP address permanently in memory for the lifetime of the master process.
  3. If DNS resolution fails (because the api_backend container 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 Gateway errors 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):

  1. Nginx skips configuration-time host resolution during boot.
  2. Nginx starts successfully even if api_backend is currently stopped or restarting.
  3. Every request queries 127.0.0.11 (cached for 10s), 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:

  1. Docker Compose initializes api_backend.
  2. Compose polls the /health endpoint until it returns HTTP 200.
  3. 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

CharacteristicStatic proxy_pass http://host:port;Dynamic proxy_pass $var; + resolver
DNS Resolution PhaseConfiguration load / container startupRuntime (per request or TTL cache)
Resolver Directive RequiredNoYes (resolver 127.0.0.11;)
Behavior on Backend CrashCrashes Nginx if backend is missing on bootNginx boots cleanly; returns 502 if down
Backend IP RegenerationPinned permanently; requires Nginx reloadAutomatically updates based on valid=10s
IPv6 Fallback SuppressionOS resolver behaviorExplicit (ipv6=off)
Configuration OverheadMinimalRequires variable declaration
Recommended EnvironmentStatic bare-metal / fixed IP topologiesDocker 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 (typically 10.96.0.10 or kube-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

FeatureFreeStarter ($5/mo)Pro ($12/mo)Agency ($29/mo)
Check Frequency15 minutes5 minutes2 minutes1 minute
Monitors1 monitor10 monitorsUnlimitedUnlimited
Multi-Location TestingGlobalGlobalGlobalGlobal
SSL & DNS AuditsBasicAdvancedReal-TimeReal-Time + Custom Root
Alert ChannelsEmailEmail, Telegram, WhatsApp, Slack, DiscordAll Channels + Multi-RecipientWebhooks, SMS, White-Label Dashboards
Multi-Tenant ManagementNoNoNoYes (Multi-Team)

Deploy real-time synthetic monitoring and container health checks today with Pingzo.

Zero-Code Uptime Alerts

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.

WhatsApp & Discord 60-Second Checks Free Forever Plan
Try Pingzo Free

Know before your users do

Connect official WhatsApp notification channels, Discord webhooks, Telegram bots, and public status pages. Start in 30 seconds.

Create Free Monitor