Nginx IPv4/IPv6 Listen Directives: When to Use Separate vs Combined Configuration


2 views

In modern Nginx configurations, the handling of dual-stack networking comes down to whether you need protocol-specific socket options. While listen [::]:80 ipv6only=off implicitly handles IPv4 via IPv6-mapped addresses, there are operational cases where separate directives are preferable:

# Combined approach (works but lacks granularity)
listen [::]:80 ipv6only=off;

# Separate directives (allows protocol-specific tuning)
listen 80 backlog=4096 deferred;
listen [::]:80 ipv6only=on so_keepalive=on;

Consider these production-grade examples where separate directives prove necessary:

# When IPv4 needs TCP optimizations absent in IPv6
listen 80 reuseport fastopen=256;
listen [::]:80 ipv6only=on reuseport;

# When applying different security policies
listen 80 proxy_protocol;
listen [::]:80 ipv6only=on ssl;

The Linux kernel's handling of IPv6 sockets with v4-mapped addresses (when ipv6only=off) introduces subtle differences:

  • Socket buffer allocation may differ between protocols
  • TCP stack behaviors (like SYN retries) can vary
  • Some kernel versions have different queue handling
# For maximum consistency across protocols
listen 80 rcvbuf=1m sndbuf=1m;
listen [::]:80 ipv6only=on rcvbuf=2m sndbuf=2m;

In containerized environments, explicit separation often works better:

# Docker/Kubernetes optimized config
listen 80 proxy_protocol bind=0.0.0.0;
listen [::]:80 ipv6only=on proxy_protocol bind=[::];

When configuring Nginx for dual-stack (IPv4/IPv6) environments, developers often encounter these two approaches:

# Approach 1: Separate directives
listen 80;
listen [::]:80 ipv6only=on;

# Approach 2: Single directive
listen [::]:80 ipv6only=off;

The fundamental difference lies in how Nginx handles socket binding:

  • ipv6only=on creates separate sockets for each protocol
  • ipv6only=off uses IPv6 socket's dual-stack capability (on supported systems)

Consider explicit separation when:

# Example: Different TCP parameters per protocol
listen 80 deferred reuseport fastopen=5;
listen [::]:80 ipv6only=on reuseport fastopen=10;

Or when needing protocol-specific logging:

server {
    listen 80;
    listen [::]:80 ipv6only=on;
    
    set $log_prefix_ipv4 "";
    set $log_prefix_ipv6 "";
    
    if ($server_port = 80) { set $log_prefix_ipv4 "IPv4-"; }
    if ($server_port = [::]:80) { set $log_prefix_ipv6 "IPv6-"; }
    
    access_log /var/log/nginx/access.log combined;
}

Single directive (ipv6only=off) generally offers:

  • Reduced kernel socket allocation
  • Simpler connection handling
  • Lower memory overhead

Important exceptions where separate directives are mandatory:

  • Older Linux kernels (< 3.9) without proper IPv6 dual-stack support
  • BSD systems with different IPv6 implementation
  • When binding to specific interfaces (listen 192.0.2.1:80 + [2001:db8::1]:80)

For most modern deployments:

# Preferred configuration for Linux kernels ≥ 3.9
listen [::]:80 ipv6only=off;

# Fallback for older systems (optional)
listen 80;

This provides the cleanest implementation while maintaining compatibility. The fallback line ensures IPv4 connectivity if the system doesn't properly support IPv6 dual-stack sockets.

Verify your configuration with:

ss -tulnp | grep nginx
netstat -tulnp | grep nginx  # For older systems

Look for separate IPv4 and IPv6 sockets when using separate directives, or a single IPv6 socket with dual-stack capability when using ipv6only=off.