2026年11月12日

2026年11月12日

Nginx TCP/UDP Proxy with the stream Module: MySQL and DNS Examples

This article is a localized edition of the Japanese original. Read the Japanese original

Nginx can relay (proxy) not only HTTP but also raw TCP and UDP traffic to other servers. The stream module provides this feature. You can spread MySQL/MariaDB connections across several database servers, or distribute DNS queries across several DNS servers. This article covers checking for the module, where to put the configuration, TCP and UDP examples, logging and access control. It assumes Nginx (stable) and the Ubuntu, Debian and RHEL-family packages as of October 2026.

Step 1: Check that the stream module is available

When you build from source, the stream module is not included by default. Check whether your installed Nginx can use it.

nginx -V 2>&1 | tr ' ' '\n' | grep stream

If you see --with-stream or --with-stream=dynamic, it is available. With =dynamic, the module is often shipped as a separate package, so install it.

# Ubuntu / Debian (the distribution's nginx package)
sudo apt install libnginx-mod-stream

# RHEL / AlmaLinux / Rocky Linux
sudo dnf install nginx-mod-stream

These packages also add the configuration that loads the module (load_module) automatically when installed. The official packages from nginx.org have the stream module built in.

Step 2: Decide where the stream block goes

The stream block goes at the same level as the http block (the outermost level of nginx.conf). It cannot be placed inside the http block.

In many packages, /etc/nginx/conf.d/*.conf and /etc/nginx/sites-enabled/* are included inside the http block. Putting a stream configuration file there causes an error, so create a separate directory for stream and include it at the outermost level of nginx.conf.

# /etc/nginx/nginx.conf (excerpt)
events {
    worker_connections 1024;
}

http {
    # ... existing web server settings (leave unchanged) ...
    include /etc/nginx/conf.d/*.conf;
}

# Add outside the http block
stream {
    log_format stream_basic '$remote_addr [$time_local] $protocol $status '
                            '$bytes_sent $bytes_received $session_time '
                            '"$upstream_addr" $upstream_connect_time';
    access_log /var/log/nginx/stream-access.log stream_basic;

    include /etc/nginx/stream.d/*.conf;
}
sudo mkdir -p /etc/nginx/stream.d

HTTP server blocks (the ones with server_name and location) stay inside the http block as before. A server block inside stream is for TCP/UDP, and server_name and location cannot be used there.

log_format is defined separately for http and stream. The main variables available in stream are:

Variable Meaning
$remote_addr Client IP address
$protocol TCP or UDP
$status Session result (200 means completed normally, 502 means the backend could not be reached, and so on)
$bytes_sent / $bytes_received Bytes sent to and received from the client
$session_time Session duration (seconds)
$upstream_addr The server the traffic was relayed to
$upstream_connect_time Time taken to connect to the upstream

Step 3: Set up a TCP proxy (MySQL example)

This example routes MySQL connections from a WordPress server through Nginx and switches to a standby server if the primary goes down.

# /etc/nginx/stream.d/mysql.conf
upstream mysql_backend {
    server 192.168.1.21:3306 max_fails=3 fail_timeout=30s;
    server 192.168.1.22:3306 backup;
}

server {
    listen 3306;
    proxy_pass mysql_backend;
    proxy_connect_timeout 5s;
    proxy_timeout 10m;

    # Restrict which clients may connect
    allow 192.168.1.0/24;
    deny all;
}
  • Writing only the port number, as in listen 3306;, listens on TCP. There is no tcp parameter for TCP. Writing listen 3306 tcp; fails with invalid parameter "tcp"
  • max_fails=3 fail_timeout=30s means that if connecting fails 3 times within 30 seconds, the server is not used for 30 seconds
  • A server marked backup is used only when all the other servers are unavailable
  • proxy_connect_timeout is how long to wait for the backend connection (60 seconds by default), and proxy_timeout is how long a connection may stay idle before it is closed (10 minutes by default). For uses that hold connections open for a long time, such as databases, setting it too short causes disconnects
  • allow and deny restrict which clients can connect. Limit the allowed range so the database port is not exposed to the internet

In open-source Nginx, monitoring of upstream servers is only passive, based on real connection failures. Active health checks that probe servers periodically are a feature of the commercial NGINX Plus.

Note that in a MySQL replication setup, writes are only possible on the primary. Design the setup for your use case so that writes are not routed to read-only replicas.

Step 4: Set up a UDP proxy (DNS example)

To listen on UDP, add udp to listen. DNS may send large responses over TCP, so listen on both TCP and UDP.

# /etc/nginx/stream.d/dns.conf
upstream dns_backend {
    server 192.168.1.31:53;
    server 192.168.1.32:53;
}

# UDP port 53
server {
    listen 53 udp;
    proxy_pass dns_backend;
    proxy_responses 1;
    proxy_timeout 5s;
}

# TCP port 53
server {
    listen 53;
    proxy_pass dns_backend;
    proxy_timeout 30s;
}

UDP has no end-of-connection, so Nginx decides when a session ends based on how long it waits for responses and how many it receives. proxy_responses 1; says "one response per query", and the session ends as soon as the response arrives. Without it, sessions linger for the whole proxy_timeout and consume resources when there are many queries.

Step 5: Pass the client IP address to the backend

With a stream proxy, the backend sees Nginx's IP address as the client. This is a problem for services that behave differently depending on the client IP, such as mail servers. If the backend supports the PROXY protocol, proxy_protocol on; passes the original client on.

# /etc/nginx/stream.d/smtp.conf
upstream smtp_backend {
    server 192.168.1.41:25;
}

server {
    listen 25;
    proxy_pass smtp_backend;
    proxy_protocol on;
}

If the backend does not have PROXY protocol support enabled, connections are not handled correctly. Postfix, for example, needs its own setting to accept it. Do not add proxy_protocol on; for services that do not support it.

Step 6: Apply and verify the settings

# Check the syntax
sudo nginx -t

# Reload the configuration without dropping connections
sudo systemctl reload nginx

# Check the listening ports (TCP and UDP)
sudo ss -lntup | grep nginx

If nginx -t reports "stream" directive is not allowed here, you put the stream block inside the http block. If it reports unknown directive "stream", the stream module is not loaded (see Step 1).

To test the connection, use for example dig @nginx-ip-address example.com for DNS or mysql -h nginx-ip-address -u username -p for MySQL. The results are logged in /var/log/nginx/stream-access.log.

Caveats

  • To listen on ports below 1024 (such as 25 and 53), the Nginx master process must start with root privileges. This works as is with the normal package startup
  • If other software (a local DNS server, MySQL itself and so on) is already using the same port, Nginx will not start. Check with ss -lntup
  • In the firewall, open only the new listening ports, and only to the clients that need them
  • On systems with SELinux enabled, Nginx may be denied from listening on non-standard ports or from connecting to backends. If you get errors, check /var/log/audit/audit.log

Summary

  • Check for the stream module with nginx -V; if it shows =dynamic, install libnginx-mod-stream or nginx-mod-stream
  • Put the stream block outside the http block, and keep its files in a dedicated directory that http does not include
  • For TCP, write only the port, as in listen 3306;. There is no tcp parameter. For UDP, use listen 53 udp;
  • For UDP, end sessions quickly with proxy_responses and a short proxy_timeout
  • Restrict clients with allow and deny, and pass the client address with proxy_protocol on; only when the backend supports it

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲