Fixing Nginx Reverse Proxy in Docker: Correct Client IP Logging with X-Forwarded-For


2 views

When running Nginx as a reverse proxy in Docker containers, one common frustration emerges: your access logs fill up with Docker's internal IP addresses (like 172.17.0.1) instead of the actual client IPs. This breaks analytics, security monitoring, and basic debugging capabilities.

The root cause lies in how Docker's networking handles traffic:

  1. External requests first hit the Docker host
  2. Docker routes them through its internal bridge network
  3. By default, Nginx sees the last hop (Docker gateway) as the client

Here's a battle-tested configuration that works with modern Docker setups:

http {
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

    server {
        listen 80;
        server_name yourdomain.com;

        # Trust Docker's internal network
        set_real_ip_from 172.17.0.0/16;
        set_real_ip_from 172.18.0.0/16;
        
        # Handle Cloudflare or other proxies if needed
        set_real_ip_from 173.245.48.0/20;
        
        real_ip_header X-Forwarded-For;
        real_ip_recursive on;

        location / {
            proxy_pass http://app-container:8080;
            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;
        }

        access_log /var/log/nginx/access.log main;
    }
}

1. Network Ranges: The set_real_ip_from directives should cover:

  • Docker's default bridge network (172.17.0.0/16)
  • Custom bridge networks if used (often 172.18.0.0/16)
  • Any external proxies (like Cloudflare)

2. Headers Handling:

real_ip_header X-Forwarded-For;
real_ip_recursive on;

This tells Nginx to:
- Use the X-Forwarded-For header
- Parse through all proxies recursively until finding an untrusted IP

If you're still seeing internal IPs:

  1. Verify headers: Add add_header X-Debug-IP $remote_addr; temporarily
  2. Check network ranges: Run docker network inspect bridge
  3. Test with curl: curl -H "X-Forwarded-For: 1.2.3.4" http://yourdomain

For complex setups (Docker → Cloudflare → Nginx):

set_real_ip_from 172.17.0.0/16;
set_real_ip_from 103.21.244.0/22;
# Add all Cloudflare IP ranges
real_ip_recursive on;
real_ip_header CF-Connecting-IP; # Cloudflare specific

This ensures correct IP handling through multiple proxy layers.


When running Nginx as a reverse proxy in Docker, you'll often notice your access logs showing Docker's internal gateway IP (typically 172.17.0.1) instead of the actual client IP. This happens because Docker's networking stack acts as an intermediate layer.

Here's what happens under the hood:

1. Client → Docker Host → Nginx Container → Backend Service
2. Without proper configuration, Nginx only sees Docker's bridge network as the "client"

# Typical incorrect log entry
172.17.0.1 - - [24/May/2016:19:50:18 +0000] "GET /admin/ HTTP/1.1" 200 19243

Here's a working configuration that properly handles client IPs:

server {
    listen 80;
    server_name example.com;

    # Trust Docker networks as proxies
    set_real_ip_from 172.17.0.0/16;
    set_real_ip_from 192.168.0.0/16;
    set_real_ip_from 10.0.0.0/8;
    
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    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;

    location / {
        proxy_pass http://backend-service:8080;
    }
}

set_real_ip_from: Defines trusted proxy IP ranges (include all Docker networks)
real_ip_header: Specifies which header contains the real IP (X-Forwarded-For)
real_ip_recursive: Enables proper IP extraction from the chain

For Kubernetes environments, you'll need to adjust the trusted IP ranges:

set_real_ip_from 10.0.0.0/8;  # Common Kubernetes CIDR
real_ip_header X-Original-Forwarded-For;  # Some ingress controllers use this

After applying changes, verify with:

docker exec -it nginx-container nginx -t
docker exec -it nginx-container nginx -s reload

Make a test request and check your logs - you should now see the actual client IP instead of Docker's gateway address.