Setting up Jellyfin and Chromecast using Docker, Nginx, and dnsmasq
- 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
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 (126.96.36.199,188.8.131.52)
- 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.
Guide Specific Requirements:
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.
Installing Jellyfin and Nginx
We will setup a docker compose file to handle Jellyfin and NGINX
Create the necessary directories:
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.
Create the Nginx Dockerfile
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.
Create the Nginx configuration file
This config will need to be updated:
- Replace mentions of
yourdomain.comwith your actual domain name.
- Once jellyfin is running, update the
Content-Security-Policyheader with the appropriate
cast_sender.jsis not loading, it will be obvious in the console errors in the
jellyfinweb ui. (will remind again at the end of the guide). These
cast_sender.jslinks may need to be updated as
jellyfinis updated. (This will work without
add_header Content-Security-Policy, but do you trust code running on your local network that much?)
Copy the below config into
Create the docker-compose file
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
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
Create the SSL Certificate using certbot
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
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... ...
Let us spin up another
nginx container and setup the SSL certificate.
Open up a new terminal and navigate to our project directory
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 email@example.com --preferred-challenges=dns
yourdomain.comwhile preserving the
*.wildcard in front of it.
firstname.lastname@example.org 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
Create a custom DNS record
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
Once this is done, press
Enter to continue.
certbot will place the created SSL certificates into
exit to shut down the container and return to our project directory.
Verify server is running
docker ps to verify that
jellyfin_web containers are running and the status is healthy. You may also look at the output of
Set up our DNS server
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.
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=184.108.40.206 server=220.127.116.11
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
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
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.
Restart the dnsmasq service
service dnsmasq restart
Ensure it is active and running
service dnsmasq status
Block Chromecast's hardcoded DNS server
To block Chromecast's access to
18.104.22.168 and instead use your own DNS server you need to login to your router admin panel and find the
TP-Link AX1800 router, the route entry would look as follows:
Network Destination: 22.214.171.124 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
126.96.36.199 is now redirected to your router.
Set your router's Primary DNS to your local DNS server
188.8.131.52 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.
Ensure Content-Security-Policy is correct
Start up your Chrome browser and ensure the dev tools are open. ( right click -> inspect )
Network tab ensure
Disable Cache is enabled.
Switch to the
Console tab and then enter in
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
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?
Configure jellyfin networking settings
Access the Networking settings by going to Dashboard - > Networking
LAN Networks to your local area network range with slash notation. Example:
Known Proxies to your NGINX Proxy server name,
Do not change any HTTPS settings, it is not necessary with the nginx proxy
jellyfin.yourserver.com isn't loading
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
jellyfin's networking HTTPS settings are disabled
Google Cast isn't showing up in the cast list
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.
I get an error saying Chromecast is unable to communicate with my
Your Chromecast likely still has access to the public
184.108.40.206 DNS server. Double check your router settings.
nmap 220.127.116.11 should fail
jellyfin acts like it is playing, but the screen is black
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.