Running OwnTone behind a Proxy
OwnTone is a tricky software to run behind a proxy. In this post, I explain how to expose it with an ingres-nginx proxy in a Kubernetes cluster.
What is OwnTone?
OwnTone is a media player that can stream content to multiple AirPlay 2 targets. It can get its media from a variety of sources. I personally use OwnTone to stream my Spotify music to my HomePods. For that, I use an add-on created by me. More details can be found in the post on the Home Assistant Community.
Why do I need a Proxy?
I am a big Kubernetes enthusiast and love to host everything the “cloud native” way. This is also the case for my Home Lab. I am running a local k3s cluster that was installed by default on my TrueNAS SCALE installation.
Doing that, I really love to use domains wherever possible. Instead of accessing my Home Assistant instance through the local IP http://192.168.178.2:8123
, I use http://ha.babel.sh
. This makes things more easy, and the big benefit is that I can take advantage of things like SSL and remote access.
What makes OwnTone special?
Getting back to the topic of this post. OwnTone is not trivial to be hosted behind a reverse proxy. Normally, it would be a simple Ingress configuration inside Kubernetes and the reference to the local IP of OwnTone through a Service and Endpoint resource. But this software is different because it uses two separate ports. The first is 3689
, which is the port for the web interface. And there is a second port 3688
, which is used to establish a web socket connection. In praxis, this web socket is only used to push notifications to the frontend. That means, in theory, I could run OwnTone without the web socket and ignore the benefit of having some push notifications. But in practice, OwnTone just stops working completely, when the web socket connection cannot be established. Even though all features regarding the controlling of the UI are implemented through the web interface and not the socket.
The big issue with this two-port design is, that the web socket port is incompatible with normal reverse proxies. When OwnTone is behind a reverse proxy, the frontend tries to connect to e.g. https://owntone.example.com:3688
. This is unexpected as normal reverse proxies only listen on :443
and :80
.
Possible solutions
To tackle that, there are a few options to consider:
Running the web frontend only through the IP address
This is the simplest solution. Just live with the fact that the IP address needs to be used to access OwnTone. But I would not write the blog if the story ends here.
Proxying 3688
with Traefik
The second idea that came to my mind was proxying the 3688
port. In my setup I used Traefik for that. I just had Traefik running with a custom configuration. But this was also not ideal because I use ingress-nginx in production and disliked the extra overhead for this issue.
Changing the port through Proxy Magic
And this is the final piece, why I am writing this blog post. I found a way by using ingress-nginx
and a small “configuration hack”.
My Solution
I found out that OwnTone's frontend receives the information regarding the web socket port through an API call. When the UI loads, a request is made to /api/config
and it looks as follows:
As you can see, the JSON data includes the websocket_port
. Afterwards, I checked the source code to find out, how this variable is used.
As you can see, the websocket_port
variable is directly used to build the wsUrl
. Hence, I came up with an idea: What if I injected a custom path through the websocket_port
? Through some more research in the code, I found out that there is no validation regarding the type. Therefore, I could try to replace the port with the default SSL port and a path:
{
...
- "websocket_port": 3688,
+ "websocket_port": "443/ws",
...
}
My configuration
For my production deployment, I use the following Ingress configuration:
The second Ingress has a custom annotation that allows to give nginx a code snippet. In this snippet, the response of /api/config
is hardcoded. I only adjusted the websocket_port
variable. Everything else is the same as the API sends to the frontend.
Additionally, I recommend using the auth-url
feature of ingress-nginx. With that, I secured OwnTone with my Single-Sign-On service and only authorized users can access the service, even though OwnTone isn't supporting user-based authentication.
Conclusion
I hope I can help someone with the same problem. Just use the Ingress configuration and your OwnTone is accessible through ingress-nginx 🎉