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:
- External requests first hit the Docker host
- Docker routes them through its internal bridge network
- 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:
- Verify headers: Add
add_header X-Debug-IP $remote_addr;
temporarily - Check network ranges: Run
docker network inspect bridge
- 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.