How to Configure Nginx WebSocket Proxying on RHEL 7

WebSockets provide a full-duplex communication channel over a single TCP connection, enabling real-time features such as live chat, push notifications, collaborative editing, and live dashboards. Unlike traditional HTTP requests, a WebSocket connection is established through an HTTP upgrade handshake and then kept open indefinitely. Nginx can act as a reverse proxy for WebSocket servers — but doing so correctly requires specific configuration: the Upgrade and Connection headers must be forwarded, HTTP/1.1 must be enforced for the upstream connection, and appropriate timeouts must be set to keep long-lived connections alive. This tutorial covers everything you need to proxy WebSocket traffic through Nginx on RHEL 7, including SELinux considerations and testing with wscat or websocat.

Prerequisites

  • RHEL 7 server with root or sudo access.
  • Nginx installed and running (install via EPEL: sudo yum -y install nginx).
  • A WebSocket application server listening on a local port (e.g., a Node.js ws server on port 3000).
  • Basic understanding of Nginx server block configuration.
  • Node.js installed if you want to run the example WebSocket server for testing.

Step 1: Install Nginx on RHEL 7

Nginx is available via EPEL on RHEL 7. If not already installed:

sudo yum -y install epel-release
sudo yum -y install nginx
sudo systemctl enable nginx
sudo systemctl start nginx

Verify Nginx is running:

sudo systemctl status nginx
curl -I http://localhost

Step 2: Understand WebSocket Upgrade Headers

A WebSocket connection starts as a standard HTTP/1.1 request with two critical headers:

  • Upgrade: websocket — tells the server that the client wants to switch protocols.
  • Connection: Upgrade — instructs Nginx (and any intermediate proxy) to forward the Upgrade header rather than consuming it.

By default, Nginx strips hop-by-hop headers (including Upgrade and Connection) before forwarding requests upstream. You must explicitly re-add them. Additionally, WebSockets require HTTP/1.1 — Nginx’s default upstream protocol is HTTP/1.0, which does not support the Upgrade mechanism, so you must override this with proxy_http_version 1.1.

Step 3: Configure Nginx for WebSocket Proxying

Open or create the Nginx configuration file for your site. A common location on RHEL 7 is /etc/nginx/conf.d/websocket.conf:

sudo vi /etc/nginx/conf.d/websocket.conf

Add the following configuration:

server {
    listen 80;
    server_name ws.example.com;

    location /ws/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        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_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 60s;
    }
}

Key directives explained:

  • proxy_http_version 1.1: Forces Nginx to use HTTP/1.1 when communicating with the upstream, which is required for the Upgrade handshake.
  • proxy_set_header Upgrade $http_upgrade: Passes the client’s Upgrade: websocket header through to the backend.
  • proxy_set_header Connection "upgrade": Overrides the default Connection: close that Nginx would otherwise send.
  • proxy_read_timeout 86400s: Keeps the connection alive for up to 24 hours without data. Without this, Nginx will close idle WebSocket connections after the default 60-second read timeout.

Step 4: Use a map Block for Dynamic Upgrade Handling

For more robust handling, especially when your Nginx proxy serves both regular HTTP and WebSocket traffic on the same upstream, use a map block to dynamically set the Connection header. This avoids sending Connection: upgrade for non-WebSocket requests, which would be invalid.

Add this block inside the http {} context in /etc/nginx/nginx.conf (or in a separate include file):

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 80;
        server_name ws.example.com;

        location /ws/ {
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;

            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;

            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_read_timeout 86400s;
            proxy_send_timeout 86400s;
        }

        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
        }
    }
}

The map block evaluates $http_upgrade: if the client sends an Upgrade header (WebSocket), $connection_upgrade becomes upgrade; if not, it becomes close. This is the pattern recommended in the official Nginx documentation.

Step 5: Configure Timeout Settings

WebSocket connections are long-lived by nature. Without proper timeout configuration, Nginx will terminate them prematurely. The three relevant timeout directives are:

proxy_read_timeout 86400s;   # How long to wait for data from upstream (24h)
proxy_send_timeout 86400s;   # How long to wait when sending data to upstream (24h)
proxy_connect_timeout 60s;   # How long to wait when establishing the upstream TCP connection

If your application sends periodic pings (heartbeats) to keep the connection alive, you may be able to use a shorter proxy_read_timeout — just make sure it exceeds the interval between heartbeats. A common pattern is to send a ping every 30 seconds and set the timeout to 120 seconds.

Step 6: Configure SELinux Booleans for Nginx Proxying

On RHEL 7 with SELinux in enforcing mode, Nginx is blocked from making outbound network connections by default. This will cause WebSocket proxy connections to fail silently. Enable the necessary boolean:

sudo setsebool -P httpd_can_network_connect 1

Verify the boolean is set:

getsebool httpd_can_network_connect

Expected output: httpd_can_network_connect --> on

If your WebSocket backend listens on a non-standard port, you may also need to allow that port:

sudo semanage port -a -t http_port_t -p tcp 3000

Verify the port is now labeled correctly:

sudo semanage port -l | grep http_port_t

Step 7: Test the Nginx Configuration and Reload

sudo nginx -t
sudo systemctl reload nginx

If nginx -t returns errors, review the output carefully — common issues include mismatched braces, incorrect directive placement, or the map block being inside a server block instead of the http block.

Step 8: Set Up a Simple WebSocket Test Server

To verify proxying is working end-to-end, create a minimal Node.js WebSocket echo server. First install Node.js:

sudo yum -y install nodejs npm

Install the ws package:

mkdir -p ~/wstest && cd ~/wstest
npm init -y
npm install ws

Create server.js:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', function connection(ws) {
    console.log('Client connected');
    ws.on('message', function incoming(message) {
        console.log('Received: %s', message);
        ws.send('Echo: ' + message);
    });
    ws.on('close', function() {
        console.log('Client disconnected');
    });
});

console.log('WebSocket server listening on port 3000');

Start the server:

node server.js &

Step 9: Test with wscat or websocat

Install wscat globally via npm:

sudo npm install -g wscat

Connect to your WebSocket endpoint through Nginx:

wscat -c ws://ws.example.com/ws/

Type a message and press Enter. The echo server should respond with Echo: your message. If you prefer a statically compiled binary, websocat is an alternative:

# Download websocat binary for Linux x86_64
curl -L https://github.com/vi/websocat/releases/latest/download/websocat.x86_64-unknown-linux-musl 
  -o /usr/local/bin/websocat
chmod +x /usr/local/bin/websocat

# Test the connection
websocat ws://ws.example.com/ws/

You can also verify the WebSocket upgrade handshake using curl with verbose output:

curl -i -N 
  -H "Connection: Upgrade" 
  -H "Upgrade: websocket" 
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" 
  -H "Sec-WebSocket-Version: 13" 
  http://ws.example.com/ws/

A successful upgrade returns HTTP 101 Switching Protocols.

Conclusion

Nginx WebSocket proxying on RHEL 7 requires three key elements: proxy_http_version 1.1 to support the upgrade handshake, explicit forwarding of the Upgrade and Connection headers, and sufficiently long timeout values to prevent Nginx from closing idle long-lived connections. The map block pattern is the cleanest approach for mixed HTTP/WebSocket traffic because it dynamically sets the Connection header based on whether an upgrade is being requested. Remember to enable the httpd_can_network_connect SELinux boolean, which is otherwise the most common reason WebSocket proxying silently fails on RHEL 7.