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 notcpparameter for TCP. Writinglisten 3306 tcp;fails withinvalid parameter "tcp" max_fails=3 fail_timeout=30smeans that if connecting fails 3 times within 30 seconds, the server is not used for 30 seconds- A server marked
backupis used only when all the other servers are unavailable proxy_connect_timeoutis how long to wait for the backend connection (60 seconds by default), andproxy_timeoutis 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 disconnectsallowanddenyrestrict 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, installlibnginx-mod-streamornginx-mod-stream - Put the
streamblock outside thehttpblock, and keep its files in a dedicated directory thathttpdoes not include - For TCP, write only the port, as in
listen 3306;. There is notcpparameter. For UDP, uselisten 53 udp; - For UDP, end sessions quickly with
proxy_responsesand a shortproxy_timeout - Restrict clients with
allowanddeny, and pass the client address withproxy_protocol on;only when the backend supports it
Related articles:
- How to use the Nginx upstream module: balancing methods, failure behavior and keepalive
- How to set up load balancing in Nginx
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)