How to Set Up Nginx as a Reverse Proxy for Redis

How to Set Up Nginx as a Reverse Proxy for Redis

Redis is a bit of an odd one to “reverse proxy” compared to the other services in this series, and it’s worth addressing that head-on before diving into configuration. Redis doesn’t speak HTTP — it uses its own binary-safe RESP (REdis Serialization Protocol) over a raw TCP connection. That means the standard proxy_pass http://... approach used for NiFi, Superset, Tomcat, or Solr simply doesn’t apply here. Instead, Nginx needs to operate at Layer 4 (TCP), using its stream module rather than the http module.

This guide explains exactly how that works, why it’s different from typical reverse proxying, and how to set it up correctly and securely.

Why Redis Needs a Different Kind of Proxy

Nginx’s http module — the one used for NiFi, Tomcat, Solr, and similar HTTP services — parses HTTP requests, inspects headers, and routes based on paths. Redis clients don’t send HTTP requests; they open a persistent TCP connection and exchange RESP-encoded commands and responses directly.

To proxy that kind of traffic, Nginx needs its stream module (ngx_stream_core_module), which does raw TCP (and UDP) forwarding without trying to interpret the payload as HTTP. This is often referred to as a “reverse proxy” in casual usage, but more precisely it’s a TCP load balancer / TCP proxy. The distinction matters because the entire toolkit of HTTP-layer features — headers, cookies, path-based routing, URL rewriting — simply isn’t available for Redis traffic through Nginx.

What Nginx can still do for Redis traffic:

  • TCP-level load balancing across multiple Redis instances or replicas.
  • TLS termination (stream supports ssl_preread and native TLS termination) if Redis itself isn’t configured with TLS.
  • IP-based access control and connection limiting.
  • A stable, single endpoint in front of a Redis Sentinel or replica set.

What it can’t do: content inspection, command-level filtering, or anything that requires understanding the Redis protocol itself.

Requirements

  • Redis installed and running, reachable on its default port 6379.
  • Nginx built with the stream module. Most distro packages include this by default, but it’s worth confirming:
nginx -V 2>&1 | grep -- '--with-stream'

If it’s missing, install the module (Debian/Ubuntu):

sudo apt install nginx-extras -y

or on RHEL-based systems, ensure the nginx-mod-stream package is installed:

sudo dnf install nginx-mod-stream -y
  • A private network the proxy will live on — Redis proxying is almost always an internal use case, not a public-facing one.
  • If TLS termination is desired, a valid certificate is needed the same way as any other Nginx TLS setup.

Step 1: Enable the Stream Module

The stream block is a top-level directive, separate from http, and needs to live in nginx.conf itself (not inside a server block under sites-available, which only applies to http).

Open the main config:

sudo nano /etc/nginx/nginx.conf

Add a stream block alongside the existing http block (not nested inside it):

stream {
    include /etc/nginx/stream.d/*.conf;
}

Create the directory for stream-specific configs:

sudo mkdir -p /etc/nginx/stream.d

Step 2: Write the TCP Proxy Configuration

Create the Redis stream config:

sudo nano /etc/nginx/stream.d/redis.conf

Basic single-backend TCP proxy:

server {
    listen 6380;
    proxy_pass 127.0.0.1:6379;
    proxy_timeout 300s;
    proxy_connect_timeout 5s;
}

This listens on port 6380 and forwards raw TCP traffic to the actual Redis instance on 6379. The port offset here is intentional — it keeps the direct Redis port distinct from the proxied one during testing, though in most real deployments the proxy would listen on 6379 externally while Redis itself binds to 127.0.0.1:6379 internally, with no port conflict since they’re on different interfaces.

For load balancing across a Redis replica set (read replicas, for instance):

upstream redis_replicas {
    least_conn;
    server 10.0.1.11:6379 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:6379 max_fails=3 fail_timeout=10s;
    server 10.0.1.13:6379 max_fails=3 fail_timeout=10s;
}

server {
    listen 6380;
    proxy_pass redis_replicas;
    proxy_timeout 300s;
    proxy_connect_timeout 5s;
}

For TLS termination in front of a Redis instance that doesn’t have TLS enabled itself:

server {
    listen 6380 ssl;
    proxy_pass 127.0.0.1:6379;

    ssl_certificate     /etc/letsencrypt/live/redis.internal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/redis.internal.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    proxy_timeout 300s;
    proxy_connect_timeout 5s;
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Step 3: Testing the Setup

The redis-cli tool can connect directly through the proxy port to confirm forwarding works:

redis-cli -h 127.0.0.1 -p 6380 ping

A response of PONG confirms the TCP proxy is forwarding traffic correctly to the backend Redis instance.

For a TLS-terminated setup, use the --tls flag (Redis 6+ CLI):

redis-cli -h redis.internal.example.com -p 6380 --tls ping

To confirm load balancing across multiple replicas is working, check connection distribution using Nginx’s stream status if stream status is compiled in, or simply monitor INFO clients on each backend Redis node to see connection counts shift as expected.

Troubleshooting

Connection refused on the proxy port Confirm the stream block is actually being loaded — it must be a top-level block in nginx.conf, not inside http. A common mistake is placing the stream server config inside sites-enabled, which Nginx never reads outside the http context.

sudo nginx -T | grep -A5 "stream {"

Nginx fails to start with “unknown directive: stream” This means Nginx wasn’t compiled with the stream module. Check with nginx -V as shown earlier, and install the appropriate module package for the distribution.

Connections work but hang or time out under load Check proxy_timeout, since Redis connections are often long-lived (especially with pub/sub or blocking commands like BLPOP). The default stream timeout may be too aggressive for these use patterns — increase it as needed.

TLS handshake failures Confirm the client actually supports TLS for Redis connections (Redis 6.0+ with the --tls CLI flag, or a client library configured for TLS). Older Redis clients that don’t support TLS at all will fail to connect to a TLS-terminated proxy port and need to either upgrade or connect through a non-TLS path on a trusted internal network instead.

Load balancing sends all traffic to one backend Verify the upstream block’s load balancing method; the default is round robin, but persistent connections (which Redis uses heavily) mean a client that connects once and stays connected will naturally stick to one backend for the life of that connection — this is expected behavior, not a bug.

Security Considerations

Redis has historically been a frequent target for opportunistic scanning and exploitation specifically because instances get left open to the internet with no authentication. A reverse/TCP proxy does not fix that by itself — it just changes where the exposed port sits.

  • Never expose the proxy port to the public internet unless Redis authentication (requirepass or Redis 6+ ACLs) and TLS are both properly configured. Treat an open Redis port — proxied or not — as equivalent to exposing a database directly.
  • Restrict by IP at the Nginx stream layer, using the allow/deny directives, which are supported inside stream server blocks just as in http:
server {
    listen 6380;
    allow 10.0.0.0/8;
    deny all;
    proxy_pass 127.0.0.1:6379;
}
  • Enable Redis authentication regardless of network-level restrictions. Set requirepass in redis.conf, or better, use Redis 6+ ACLs for per-user, per-command restrictions. The proxy layer is not a substitute for this.
  • Consider ssl_preread if TLS is already terminated at the Redis level and Nginx just needs to route based on SNI without decrypting traffic itself — this keeps end-to-end encryption intact while still allowing Nginx-level routing decisions.
  • Rename or disable dangerous commands (FLUSHALL, CONFIG, KEYS) via Redis’s own rename-command directive if the proxy is the only thing standing between an untrusted network segment and the Redis instance — though the better answer is almost always tighter network segmentation in the first place.

Performance Tips

  • Keep proxy_timeout aligned with actual usage patterns. Pub/sub and blocking commands need long timeouts; simple cache-style GET/SET workloads can use shorter ones to free up idle connections faster.
  • Use least_conn for load balancing across Redis replicas rather than round robin, since Redis connections tend to be long-lived, and round robin can create uneven load distribution over time as connections accumulate unevenly.
  • Monitor connection counts on both Nginx and Redis sides; the stream module adds negligible overhead, but an undersized worker_connections value in the Nginx events block can become a bottleneck under high concurrent connection counts.
  • Avoid unnecessary TLS termination overhead if Redis and Nginx are on the same trusted internal network — reserve TLS termination for traffic crossing untrusted network boundaries.

Real-World Use Cases

  • Providing a stable endpoint in front of a Sentinel-managed failover set, where application clients connect to one address and Nginx (or ideally Sentinel-aware client logic combined with a lightweight proxy) handles routing to the current primary.
  • Read replica load balancing, spreading read-heavy workloads across several Redis replicas through a single proxy address rather than hardcoding replica addresses into every client application.
  • Adding TLS to legacy Redis deployments that don’t have native TLS support enabled, without needing to reconfigure Redis itself.
  • Centralized IP-based access control, where Nginx’s stream layer becomes the single place network policy is enforced, instead of managing firewall rules per Redis instance.

Best Practices Checklist

  • Use the stream module, not http — Redis doesn’t speak HTTP, and trying to proxy it with proxy_pass http://... will not work.
  • Keep the stream block at the top level of nginx.conf, outside http.
  • Set realistic proxy_timeout values that account for long-lived and blocking Redis connections.
  • Always pair proxying with Redis authentication (requirepass or ACLs) — a proxy is not an authentication layer.
  • Restrict access by IP at the stream layer as a first line of defense.
  • Treat Redis as an internal-only service by default; there are very few legitimate reasons to expose it, proxied or otherwise, to the public internet.
  • Use least_conn load balancing for replica sets given Redis’s long-lived connection patterns.

Reverse proxying Redis is really TCP proxying, and treating it that way from the start avoids a lot of confusion. Nginx’s stream module handles the mechanics well, but the more important work is on the security side: making sure a proxy in front of Redis doesn’t become a false sense of protection for a service that still needs its own authentication and tight network boundaries.

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx as a Reverse Proxy for Couchbase

How to Set Up Nginx as a Reverse Proxy for Couchbase

Next Post
How to Set Up Nginx as a Reverse Proxy for Apache Solr

How to Set Up Nginx as a Reverse Proxy for Apache Solr

Related Posts