·technologytil

How to get Nginx Proxy Manager to support WebSockets

My reverse proxy setup has evolved over time and consists mainly of traefik for my Docker instances and Synology Reverse Proxy for a mosquitto instance that is intended to provide a WebSocket for a web application.

Since the Synology NAS is reaching the end of its lifecycle, I have already migrated the virtual machines to a dedicated Proxmox Virtual Environment and am now using the NAS again for its original purpose: exclusively for reliable data storage.

After having done some research, I found that Nginx Proxy Manager should be capable of taking on the tasks of both traefik and said reverse proxy functionality of my NAS, which offered a path to consolidate in one system.

The installation is pretty straightforward when following the installation guide, and after having started the instance with docker compose up -d, the administration GUI is accessible via http://<your-docker-host>:81.

Even the configuration is intuitive:

  • Click on Add Proxy Host
  • Enter the Domain Name (make sure its DNS records point to your Docker instance or to your dynamic DNS hostname when configuring external access).
  • Scheme: http
  • Forward Hostname / IP: <your-docker-instance>
  • Forward Port: 9001
  • Access List: Publicly Accessible
  • Options: Enable Block Common Exploits and Websockets Support

In the SSL tab I selected my previously uploaded SSL Certificate and enabled both Force SSL and HTTP/2 Support. The setup is finished by hitting the Save button and after verifying everything works as intended I noticed nothing to be working as intended.

MQTT Analyzer (which I like very much) showed the error message Expected MQTT CONNACK, but received a HTTP response. The TLS certificate chain seemed to be correct. Just to be sure I checked with:

bash
openssl s_client -connect <websocket-instance>:443 -servername <websocket-instance>

The response is supposed to be something like

Connecting to xxx.xxx.xxx.xxx
CONNECTED(00000005)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=E7
verify return:1
depth=0 CN=example.com
verify return:1
---
Certificate chain

On my Docker host, I ran the following command to check the connection to my mosquitto instance, which provides the WebSocket:

bash
docker exec nginx-proxy-manager curl -v \
  -H "Upgrade: websocket" \
  -H "Connection: Upgrade" \
  -H "Sec-WebSocket-Key: c2FtcGxlLW5vbmNl" \
  -H "Sec-WebSocket-Version: 13" \
  http://172.25.0.10:9001

And the response was:

* Empty reply from server
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Closing connection 0
curl: (52) Empty reply from server

So, long story short: The server is closing the connection right away. Although I’ve enabled the WebSockets support, the connection is never upgraded to a WebSocket connection.

In the settings of my proxy host, I’ve found a gear icon to add my own configuration directives, and I read a lot about adding lines like these:

bash
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Accept-Encoding "";

I'm pretty convinced this is unnecessary. When enabling the above-mentioned WebSockets support, the following lines are already added to the proxy host configuration file which I checked by running:

bash
docker exec nginx-proxy-manager less /data/nginx/proxy_host/<number-of-proxy-host-configuration>.conf

The output was:

[...]
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
[...]

So, this seemed to be a red herring.

After hours of reading and trying, I’ve found that there might be a mosquitto quirk which requires adding the header Sec-Websocket-Protocol: mqtt to the request:

bash
docker exec nginx-proxy-manager curl -v \
  -H "Upgrade: websocket" \
  -H "Connection: Upgrade" \
  -H "Sec-WebSocket-Key: c2FtcGxlLW5vbmNl" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Protocol: mqtt" \
  http://172.25.0.10:9001

And the immediate response was:

Trying 172.25.0.10:9001...
* Connected to 172.25.0.10 (172.25.0.10) port 9001 (#0)
> GET / HTTP/1.1
> Host: 172.25.0.10:9001
> User-Agent: curl/7.88.1
> Accept: */*
> Upgrade: websocket
> Connection: Upgrade
> Sec-WebSocket-Key: c2FtcGxlLW5vbmNl
> Sec-WebSocket-Version: 13
> Sec-WebSocket-Protocol: mqtt
>
< HTTP/1.1 101 Switching Protocols
< Upgrade: WebSocket
< Connection: Upgrade
< Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
< Sec-WebSocket-Protocol: mqtt

Aha, now the connection is switching protocols and upgrading to WebSocket! That’s a great finding! Back to the settings of the proxy host, I clicked on the gear icon again and added:

bash
proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;

and saved it again. After adding this, it worked like a charm.

Nginx Proxy Manager