RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx Docker Host Network 111 Connection
02. Root Cause Vector

Nginx in Docker host network mode fails with errno 111 when the upstream service isn't bound to the expected interface or port, killing a...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Performance

Nginx Docker Host Network 111 Connection

Nginx in Docker host network mode fails with errno 111 when the upstream service isn't bound to the expected interface or port, killing all proxied traffic instantly.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(96L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-api-gateway
  namespace: pci-secure-zone
  labels:
    app: nginx-proxy
    tier: edge
    # TODO: Clean up after PCI audit Q4 2023
    # legacy: true
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-proxy
  template:
    metadata:
      labels:
        app: nginx-proxy
        # Workaround for service mesh injection
        sidecar.istio.io/inject: "false"
    spec:
      hostNetwork: true  # Required for legacy IP binding
      containers:
      - name: nginx
        image: my-registry.internal/legacy-nginx:1.18.0-alpine  # Pinned for compliance
        # VULNERABILITY: Upstream config points to service name, but hostNetwork bypasses kube-dns unless explicitly configured
        # The upstream backend uses the standard Kubernetes service DNS name, which resolves to cluster IP.
        # With hostNetwork: true, the container uses the host's network namespace and its DNS resolver.
        # The host's resolver does not know about 'backend-service.pci-secure-zone.svc.cluster.local'.
        # This creates an implicit, subtle conflict where the nginx config appears correct but will fail at runtime.
        env:
        - name: BACKEND_SERVICE
          value: "backend-service.pci-secure-zone.svc.cluster.local"
        - name: AWS_ACCESS_KEY_ID
          value: "AKIAIOSFODNN7EXAMPLE"
        - name: AWS_SECRET_ACCESS_KEY
          value: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
        - name: STRIPE_API_KEY
          value: "sk_live_4eC39HqLyjWDarjtT1zdp7dc"
        # Old DB URI from migration, left in case rollback needed
        - name: DATABASE_URL
          value: "postgresql://dbuser:LegacyP@ssw0rd2021!@old-db.internal.corp:5432/transactions?sslmode=disable"
        ports:
        - containerPort: 80
          hostPort: 80
          protocol: TCP
        # The following volume mount is a red herring from a previous TLS termination setup
        # volumeMounts:
        # - name: tls-certs
        #   mountPath: /etc/nginx/certs
        #   readOnly: true
        livenessProbe:
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
        # Configuration uses the env var, which will resolve incorrectly
        command:
        - /bin/sh
        - -c
        - |
          cat > /etc/nginx/conf.d/default.conf << 'EOF'
          upstream backend {
              server $BACKEND_SERVICE:8080;
              # keepalive 32;  # TODO: Tune for production
          }
          server {
              listen 80;
              # Commented out old rate limiting rule
              # limit_req zone=one burst=10 nodelay;
              location /api/ {
                  proxy_pass http://backend;
                  proxy_http_version 1.1;
                  proxy_set_header Connection "";
                  # TODO: Add X-Forwarded-For header
              }
          }
          EOF
          nginx -g 'daemon off;'
      # volumes:
      # - name: tls-certs
      #   secret:
      #     secretName: nginx-tls-secret
      #     # Note: Secret contains dummy RSA key from old Puppet manifest
      #     # -----BEGIN RSA PRIVATE KEY-----
      #     # MIIEowIBAAKCAQEA4q4h6vX5zJgZ... [truncated]
      #     # -----END RSA PRIVATE KEY-----
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Nginx 'connect() failed (111: Connection refused)' in Docker Host Network Mode

Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins

TL;DR

  • What broke: Nginx cannot reach the upstream service because the target process is bound to 127.0.0.1 only, or the port is wrong — and in host network mode, the usual Docker DNS tricks don't apply.
  • How to fix it: Ensure the upstream service binds to 0.0.0.0 (or the correct host IP), verify the port matches, and confirm --network host is actually applied to the Nginx container.
  • Use our Client-Side Sandbox below to paste your nginx.conf and docker-compose.yml and auto-generate the refactored config.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error from /var/log/nginx/error.log:

2024/01/15 03:42:17 [error] 29#29: *1 connect() failed (111: Connection refused)
while connecting to upstream, client: 10.0.0.5, server: _, request:
"GET /api/health HTTP/1.1", upstream: "http://127.0.0.1:8080/api/health",
host: "prod-gateway.internal"

errno 111 = ECONNREFUSED. The TCP SYN hit the host network stack and got a RST back. Nothing is listening on that socket. Nginx tried, the kernel rejected it. Every request hitting this upstream returns 502 Bad Gateway to your users — right now, in production.

In host network mode, the Nginx container shares the host's network namespace directly. There are no virtual bridges, no container IPs. localhost inside Nginx is the host's loopback. So if your upstream app is also on host network and bound to 127.0.0.1:8080, this should work — unless it isn't running, bound to the wrong port, or crashed silently.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This isn't a security exploit — it's a cascading availability failure:

  1. Upstream process died or never started. Nginx came up, upstream didn't. No readiness gate caught it.
  2. Upstream bound to wrong interface. App server configured to 127.0.0.1 inside its own container namespace, but then switched to host network without updating the bind address — now it's bound to a loopback that exists but the port assignment shifted.
  3. Port conflict on the host. Another process grabbed 8080 first. Your app silently failed to bind and exited non-zero — but your restart: always policy is in a crash loop you haven't noticed.
  4. --network host missing from the Nginx container. You applied it to the upstream but not Nginx. Nginx is still on the bridge network trying to reach 127.0.0.1 of its own isolated namespace — which has nothing on port 8080.

Blast radius: 100% of traffic to this upstream returns 502. If Nginx has no fallback (backup server, error_page redirect), your entire service is down.


How to Fix It

Step 1: Confirm What's Actually Listening

# On the host — not inside a container
ss -tlnp | grep 8080
# or
netstat -tlnp | grep 8080

If that returns nothing, your upstream process is not running. Fix that first.


Basic Fix — nginx.conf upstream address

In host network mode, use 127.0.0.1 explicitly (not a container hostname — DNS doesn't apply here):

upstream backend {
-    server app-container:8080;   # WRONG: container DNS doesn't resolve in host network
+    server 127.0.0.1:8080;       # CORRECT: host loopback, shared namespace
}

Basic Fix — docker-compose.yml

services:
  nginx:
    image: nginx:1.25-alpine
-   # network_mode not set — defaults to bridge
+   network_mode: host
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

  app:
    image: myapp:latest
-   # network_mode not set
+   network_mode: host
    environment:
-     - BIND_ADDR=127.0.0.1:8080   # Only reachable within its own namespace if on bridge
+     - BIND_ADDR=0.0.0.0:8080     # Bind all interfaces so host loopback reaches it

Enterprise Best Practice — Health-Gated Startup + Upstream Keepalive

Don't let Nginx start routing until the upstream is confirmed alive. Add a healthcheck and a depends_on condition, plus keepalive to avoid connection churn:

services:
  nginx:
    image: nginx:1.25-alpine
    network_mode: host
+   depends_on:
+     app:
+       condition: service_healthy
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

  app:
    image: myapp:latest
    network_mode: host
+   healthcheck:
+     test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/healthz"]
+     interval: 5s
+     timeout: 3s
+     retries: 5
+     start_period: 10s

And in nginx.conf, add upstream keepalive and failure handling:

upstream backend {
    server 127.0.0.1:8080;
+   keepalive 32;
}

server {
    location / {
        proxy_pass http://backend;
+       proxy_next_upstream error timeout http_502 http_503;
+       proxy_connect_timeout 2s;
+       proxy_read_timeout 10s;
+       proxy_http_version 1.1;
+       proxy_set_header Connection "";
    }
}

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Lint nginx.conf in your pipeline:

# In your GitHub Actions / GitLab CI step
docker run --rm -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.25-alpine nginx -t

This catches syntax errors but not runtime upstream failures. Pair it with an integration test.

2. Integration smoke test after deploy:

# Post-deploy health gate — fail the pipeline if upstream is unreachable
curl --retry 5 --retry-delay 3 --retry-connrefused -sf http://127.0.0.1:80/healthz || exit 1

3. Checkov / Trivy for compose files:

checkov -f docker-compose.yml --check CKV_DOCKER_2  # Healthcheck defined

4. OPA policy to enforce healthcheck on host-network services:

deny[msg] {
    service := input.services[name]
    service.network_mode == "host"
    not service.healthcheck
    msg := sprintf("Service '%v' uses host network but has no healthcheck defined", [name])
}

5. Use a process supervisor on the host for the upstream app (systemd, supervisord) so it restarts before Nginx gives up — don't rely solely on Docker's restart: always which has backoff delays that create 502 windows.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does 'connect() failed (111: Connection refused)' happen specifically in Docker host network mode?
In host network mode, the container shares the host's network namespace. There's no Docker DNS, no virtual bridge — `127.0.0.1` is the actual host loopback. errno 111 (ECONNREFUSED) means the TCP stack received a RST packet because nothing is listening on that port. The most common causes are: the upstream process crashed, it's bound to the wrong interface, or the `--network host` flag was applied to only one of the two containers. This is a foundational networking error that exposes the fragility of manual container orchestration.
Q2.Can I use a container name as the upstream in nginx.conf when using host network mode?
No. Container hostnames and Docker's internal DNS (e.g., `app-container:8080`) only work on bridge or overlay networks where Docker manages name resolution. In host network mode, you must use `127.0.0.1` or the explicit host IP address. Using a container name will result in a DNS resolution failure or connection refused. Manually patching these configurations across environments is a high-risk, error-prone trap that creates security vulnerabilities and increases your blast radius. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix.
Q3.How do I prevent this 502 error during container restarts or rolling deploys?
Three layers of defense: (1) Add a `healthcheck` to your upstream service in docker-compose and use `depends_on: condition: service_healthy` so Nginx only starts routing after the upstream is confirmed alive. (2) Configure `proxy_next_upstream error timeout http_502` in nginx.conf to retry on failure. (3) Add a post-deploy smoke test in your CI/CD pipeline that retries the health endpoint with `curl --retry-connrefused` before marking the deploy successful. However, implementing and maintaining this manually across large mono-repos or at scale is a compliance nightmare. We deploy the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review and governance.