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:
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:
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:
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:
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:
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:
proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;
and saved it again. After adding this, it worked like a charm.