1/03/2023
- Requirements
- Guide Specific Requirements
- Installing Docker
- Installing Jellyfin and Nginx
- Set up our DNS server
- Block Chromecast's hardcoded DNS server
- Set your router's Primary DNS to your local DNS server
- Ensure Content-Security-Policy is correct
- Configure jellyfin networking settings
- Troubleshooting
This will serve as a guide for setting up Jellyfin Web UI with Chromecast. This has proven to be a challenge because:
- Chromecast requires that HTTP traffic be encrypted into HTTPS with a valid certificate.
- Chromecast is hard coded with google DNS servers (8.8.8.8,8.8.4.4)
- Basically, Chromecast assumes that you are a 3rd party website hosted on the public internet somewhere like Youtube. Which you are not, you are a local server hosting Jellyfin. This guide will make your Jellyfin server appear as such.
The above points introduce some challenges such as:
- Creating a valid signed HTTPS certificate for a public domain name
- Making the above certificate work properly on a local network
- Rerouting Google DNS addresses to our own internal DNS server
- Creating a Content-Security-Policy so browsers do not block cross origin Chromecast js scripts
- A valid public domain name and access to its DNS records. This guide uses a domain registered on domains.google.com. Creating a subdomain / CNAME in your DNS settings is NOT necessary.
- A router that allows static routing settings (most modern routers).
- An internal DNS server on your local network. This guide uses an Ubuntu laptop as the DNS server, NGINX proxy server, and Jellyfin server.
This guide uses a specific setup that may or may not apply to your environment.
- Ubuntu 22.04 for hosting Jellyfin, a DNS server, and an NGINX proxy
- Jellyfin 10.8.8
- Docker 20.10.17 and docker-compose 1.29.2 for containerization of jellyfin and NGINX
- dnsmasq DNS server
Follow instructions here https://docs.docker.com/desktop/install/ubuntu/ and ensure the test script works. Consider linking docker-compose
(vs using docker compose
without the dash) for accuracy of the guide.
We will setup a docker compose file to handle Jellyfin and NGINX
mkdir -p ~/projects/jellyfin && cd ~/projects/jellyfin && mkdir -p cache config media nginx
This will create the jellyfin
folder in a projects folder in your home directory. It will also create the necessary docker volume
folders for jellyfin and nginx.
cat <<"EOT" >> ~/projects/jellyfin/nginx/Dockerfile
FROM nginx:latest
RUN apt-get update
RUN apt-get install -y certbot
EOT
This creates an Nginx container with certbot installed.
This config will need to be updated:
- Replace mentions of
yourdomain.com
with your actual domain name. - Once jellyfin is running, update the
Content-Security-Policy
header with the appropriatecast_sender.js
links. Ifcast_sender.js
is not loading, it will be obvious in the console errors in thejellyfin
web ui. (will remind again at the end of the guide). Thesecast_sender.js
links may need to be updated asjellyfin
is updated. (This will work withoutadd_header Content-Security-Policy
, but do you trust code running on your local network that much?)
Copy the below config into ~/projects/jellyfin/nginx/nginx.conf
.
# nginx config Borrowed from https://jellyfin.org/docs/general/networking/nginx/
user nginx; ## Default: nobody
worker_processes 1; ## Default: 1
pid /tmp/nginx.pid;
events {
worker_connections 1024; ## Default: 1024
}
http {
index index.html index.htm index.php;
server {
listen 80;
listen [::]:80;
server_name jellyfin.yourdomain.com;
# Uncomment to redirect HTTP to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jellyfin.yourdomain.com;
## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc.
client_max_body_size 20M;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.com/chain.pem;
ssl_stapling on;
ssl_stapling_verify on;
# Security / XSS Mitigation Headers
# NOTE: X-Frame-Options may cause issues with the webOS app
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
# Content Security Policy
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
# Enforces https content and restricts JS/CSS to origin
# External Javascript (such as cast_sender.js for Chromecast) must be whitelisted.
# NOTE: The default CSP headers may cause issues with the webOS app
# NOTE 2: cast_sender.js links may need to be updated. Check jellyfin console for load errors
add_header Content-Security-Policy "default-src https: data: blob: http://image.tmdb.org; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.gstatic.com/eureka/clank/108/cast_sender.js https://www.gstatic.com/eureka/clank/107/cast_sender.js https://www.gstatic.com/eureka/clank/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'";
location = / {
return 302 http://$host/web/;
#return 302 https://$host/web/;
}
location / {
# Proxy main Jellyfin traffic
proxy_pass http://jellyfin:8096;
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_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
proxy_buffering off;
}
# location block for /web - This is purely for aesthetics so /web/#!/ works instead of having to go to /web/index.html/#!/
location = /web/ {
# Proxy main Jellyfin traffic
proxy_pass http://jellyfin:8096/web/index.html;
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_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
location /socket {
# Proxy Jellyfin Websockets traffic
proxy_pass http://jellyfin:8096;
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_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
}
}
This config utilizes hardware acceleration as described here https://jellyfin.org/docs/general/administration/hardware-acceleration#hardware-acceleration-on-docker-linux
This will enable jellyfin
to use a GPU to do encoding and decoding.
In this example, I have an NVIDIA Quadro card I want to take advantage of, so I have installed the nvidia container toolkit
documented here https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#getting-started
If you do not want to utilize hardware acceleration, remove the deploy
tree in the config
Update the environment variable JELLYFIN_PublishedServerUrl
in this configuration to match your domain name.
Copy the below config into ~/projects/jellyfin/docker-compose.yml
.
version: '3.5'
services:
web:
build: nginx
container_name: jellyfin_web
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/letsencrypt:/etc/letsencrypt
- ./nginx/ssl:/etc/nginx/ssl
logging:
driver: "json-file"
options:
max-size: "500k"
max-file: "10"
jellyfin:
image: jellyfin/jellyfin
container_name: jellyfin
volumes:
- ./config:/config
- ./cache:/cache
- ./media:/media
# Optional - Hardware acceleration
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
restart: 'unless-stopped'
# Optional - alternative address used for autodiscovery
environment:
- JELLYFIN_PublishedServerUrl=https://jellyfin.yourdomain.com
First we need to run our docker-compose
file. Nginx will automatically fail because it will not be able to find the SSL certificate specified in our nginx.conf
. But we don't need to worry about that right now.
Go ahead and run docker-compose up
while in the ~/projects/jellyfin
directory
Once it loads, you'll probably see something like:
user@group:~/projects/jellyfin$ docker-compose up
Starting jellyfin_web ... done
Starting jellyfin ... done
Attaching to jellyfin_web, jellyfin
web_1 | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
web_1 | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
web_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
web_1 | 10-listen-on-ipv6-by-default.sh: info: IPv6 listen already enabled
web_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
web_1 | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
web_1 | /docker-entrypoint.sh: Configuration complete; ready for start up
web_1 | 2023/01/04 09:46:55 [emerg] 1#1: cannot load certificate...
...
Open up a new terminal and navigate to our project directory
cd ~/projects/jellyfin
Let us start a new nginx container with a bash prompt presented to us:
docker-compose run web /bin/bash
Now with certbot already installed in our Dockerfile
, we can simply run:
certbot certonly --manual -d *.yourdomain.com --agree-tos -m youremail@address.com --preferred-challenges=dns
- Replace
yourdomain.com
while preserving the*.
wildcard in front of it. - Replace
youremail@address.com
with your own email
This command will perform a DNS-01 challenge to validate you control your domain name:
Performing the following challenges:
dns-01 challenge for yourdomain.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.yourdomain.com with the following value:
917nbBrGIYv0WbvM0URhPvcWFKh5wpryZQtXtJfti_8
Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Now we need to login to our domain registrar and navigate to the DNS settings.
We will set a custom record using the info certbot
is giving us:
Host name:
_acme-challenge.yourdomain.com
Type:
TXT
TTL:
1 Hour
Data:
917nbBrGIYv0WbvM0URhPvcWFKh5wpryZQtXtJfti_8
Every registrar is different and may require some fiddling. For example, with domains.google.com
, we only want to put in _acme-challenge
into the Host name
field, as Google autofills the .yourdomain.com
afterwards.
Once this is done, press Enter
to continue.
If successful, certbot
will place the created SSL certificates into /etc/letsencrypt
.
Type exit
to shut down the container and return to our project directory.
Type docker ps
to verify that jellyfin
and jellyfin_web
containers are running and the status is healthy. You may also look at the output of docker-compose up
.
We will use dnsmasq
for our DNS service.
Keep in mind that if libvirtd
is installed, it uses its own installation of dnsmasq
. We will not be using this version, and install one that automatically sets up a service for us.
sudo apt-get install dnsmasq
You may receive and error message that the service failed to start on a specified address. This will be fixed in the next step.
Open the dnsmasq
configuration file:
sudo gedit /etc/dnsmasq.conf
We are presented with a well-commented configuration file. Luckily, we only need to worry about a few settings:
Ensure no-resolv is uncommented:
# If you don't want dnsmasq to read /etc/resolv.conf or any other
# file, getting its servers from this file instead (see below), then
# uncomment this.
no-resolv
Ensure DNS forwarding addresses are set. This example uses Quad9's DNS service:
# Add other name servers here, with domain specs if they are for
# non-public domains.
server=9.9.9.9
server=149.112.112.112
Set our jellyfin
redirect address where 192.168.0.2
is replaced your jellyfin machine's local IP address. Static/Manual IP settings on your network configuration is highly recommended:
# Add domains which you want to force to an IP address here.
# The example below send any host in double-click.net to a local
# web-server.
address=/jellyfin.yourdomain.com/192.168.0.2
Set the ethernet interface. You can find yours by using ip -br -f inet address
and looking for the name of the device that lists the correct IP.
(alternatively, you may set the listen-address
)
Whatever IP is assigned to the device (or the listen-address
) will be the local DNS server address.
# If you want dnsmasq to listen for DHCP and DNS requests only on
# specified interfaces (and the loopback) give the name of the
# interface (eg eth0) here.
# Repeat the line for more than one interface.
interface=eth0
Ensure bind-interfaces
is uncommented:
# On systems which support it, dnsmasq binds the wildcard address,
# even when it is listening on only some interfaces. It then discards
# requests that it shouldn't reply to. This has the advantage of
# working even when interfaces come and go and change address. If you
# want dnsmasq to really bind only the interfaces it is listening on,
# uncomment this option. About the only time you may need this is when
# running another nameserver on the same machine.
bind-interfaces
Ensure you save the file after done editing.
service dnsmasq restart
Ensure it is active and running
service dnsmasq status
To block Chromecast's access to 8.8.8.8
and instead use your own DNS server you need to login to your router admin panel and find the Routing
settings.
For a TP-Link AX1800
router, the route entry would look as follows:
Network Destination:
8.8.8.8
Subnet Mask:
255.255.255.255
Default Gateway (your router's address)
192.168.0.1
Interface
LAN
Description
Google DNS 1
Any device trying to access 8.8.8.8
is now redirected to your router.
Now that 8.8.8.8
is blocked, set the router's Primary DNS server
to your local DNS server address
.
Now would be a good time to restart the Chromecast if it has been on.
Start up your Chrome browser and ensure the dev tools are open. ( right click -> inspect )
In the Network
tab ensure Disable Cache
is enabled.
Switch to the Console
tab and then enter in jellyfin.yourserver.com
This should automatically load https
and display a secure connection (lock icon next to the address bar)
Once logged into your jellyfin dashboard, ensure the console log does not display any errors that look like:
Refused to load the script 'https://www.gstatic.com/eureka/clank/108/cast_sender.js' because it violates the following Content Security Policy directive...
If you do, you need to add the URL in the error to the add_header Content-Security-Policy
line in your nginx.conf
.
As mentioned before, this will work without add_header Content-Security-Policy
, but do you trust the code running on your local server that much?
Access the Networking settings by going to Dashboard - > Networking
Set LAN Networks
to your local area network range with slash notation. Example: 192.168.0.0/24
Set Known Proxies
to your NGINX Proxy server name, jellyfin.yourserver.com
Do not change any HTTPS settings, it is not necessary with the nginx proxy
Ensure docker-compose up
has been ran in your project folder
Check docker logs docker logs jellyfin
, docker logs jellyfin_web
for any errors
Ensure your device's network settings is set to use your local DNS address. You can check using nmap: nmap jellyfin.yourserver.com
and it should point to the correct local IP address.
Ensure the DNS server is running. service dnsmasq status
Ensure jellyfin
's networking HTTPS settings are disabled
Ensure you're running the site in Google Chrome or Chromium browsers
Ensure there are no errors loading cast_sender.js
in the browser console logs
Ensure your Chromecast is plugged in and powered on.
Your Chromecast likely still has access to the public 8.8.8.8
DNS server. Double check your router settings.
nmap 8.8.8.8
should fail
Check your supported bitrate. For example, my first generation Chromecast only supports bitrates up to 8 Mbps.
You can set the bitrate in user -> settings -> playback -> Google Cast Streaming Quality
. Note: This seems to only change bitrate, not resolution as implied by the dropdown menu.
I wonder how VLC works with Chromecast given all the requirements above? It seems to cast just fine without any of this, from PC or Android.