jellyfinsetup


Latest Updates: https://wiki.haacksnetworking.org/doku.php?id=computing:jellyfin

This tutorial is for Debian users wanting to set up a production-ready Jellyfin server. This instance runs on a VM inside my virsh+qemu stack recently recapped in detail in this article. This VM is set up with a LAMP stack, a reverse proxy with Let's Encrypt, automated syncing, scanning, and some hardening measures. I'm using fpm with the mpm_event handler for concurrency. My standard fail2ban setup is in place for protection. This instance is designed for private media watching. The library is imaged off my master library via a remote source and includes aggressive cover art fetching. Don't proceed before TLS/LAMP stack is in place. If you need help, visit https://wiki.haacksnetworking.org/doku.php?id=computing:apachesurvival.

First, we need to install the required packages:

sudo apt update && sudo apt upgrade -y
sudo apt install ffmpeg wget nano curl snapd ufw fail2ban postfix apache2 php8.4-fpm php8.4-mysql php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip apt-transport-https ca-certificates gnupg
sudo a2enmod ssl headers 

Once your LAMP stack is installed, edit your 000-default.conf host for your Jellyfin domain. After that, cut it a certificate before proceeding:

sudo apt install certbot letsencrypt python3-certbot-apache
sudo certbot --authenticator standalone --installer apache -d gnulinux.media --pre-hook "systemctl stop apache2" --post-hook "systemctl start apache2"

Later, we will switch the 000-default.conf virtual host to use a reverse proxy, but for now, it helps to create the certificate with the mostly stock configuration. Now, let's install Jellyfin:

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
echo "deb [arch=$( dpkg --print-architecture ) signed-by=/etc/apt/keyrings/jellyfin.gpg] https://repo.jellyfin.org/debian trixie main" | sudo tee /etc/apt/sources.list.d/jellyfin.list
sudo apt update
sudo apt install -y jellyfin
sudo systemctl enable jellyfin
sudo systemctl start jellyfin

Once Jellyfin is installed and running, let's create the media directory and set permissions:

sudo mkdir -p /srv/jellyfin/media/{Family,Movies,Friends,Presentations,Television}
sudo chown -R jellyfin:jellyfin /srv/jellyfin/media
sudo find /srv/jellyfin/media -type d -exec chmod 755 {} +
sudo find /srv/jellyfin/media -type f -exec chmod 644 {} +

Now that Jellyfin is installed and has a properly setup media directory, we can prepare and configure apache and php for the reverse proxy.

sudo a2enmod proxy proxy_http rewrite proxy_fcgi setenvif sudo a2enconf php8.4-fpm
sudo a2dismod mpm_prefork php8.4 
sudo a2enmod mpm_event

Now that the modules that the reverse proxy requires are enabled, we can safely swap out the contents of 000-default.conf and the auto-generated 000-default-le.conf. Enter something like this in the http virtual host:

<VirtualHost *:80>
 
    ServerName gnulinux.media
 
    ProxyPreserveHost On
    ProxyPass "/.well-known/" "!"
    ProxyPass "/socket" "ws://127.0.0.1:8096/socket"
    ProxyPassReverse "/socket" "ws://127.0.0.1:8096/socket"
    ProxyPass "/" "http://127.0.0.1:8096/"
    ProxyPassReverse "/" "http://127.0.0.1:8096/"
 
    RequestHeader set X-Forwarded-Proto "http"
 
    ErrorLog ${APACHE_LOG_DIR}/jellyfin_error.log
    CustomLog ${APACHE_LOG_DIR}/jellyfin_access.log combined
 
    RewriteEngine on
    RewriteCond %{SERVER_NAME} =gnulinux.media
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
 
    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
    </FilesMatch>
 
</VirtualHost>

And, for the https virtual host, something like:

<VirtualHost *:443>
 
    ServerName gnulinux.media
 
    ProxyPreserveHost On
    ProxyPass "/.well-known/" "!"
    ProxyPass "/socket" "ws://127.0.0.1:8096/socket"
    ProxyPassReverse "/socket" "ws://127.0.0.1:8096/socket"
    ProxyPass "/" "http://127.0.0.1:8096/"
    ProxyPassReverse "/" "http://127.0.0.1:8096/"
 
    RequestHeader set X-Forwarded-Proto "https"
 
    SSLEngine on
 
    ErrorLog ${APACHE_LOG_DIR}/jellyfin_error.log
    CustomLog ${APACHE_LOG_DIR}/jellyfin_access.log combined
 
    SSLCertificateFile /etc/letsencrypt/live/gnulinux.media/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/gnulinux.media/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
 
    <FilesMatch ".+\.ph(ar|p|tml)$">
        SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
    </FilesMatch>
 
</VirtualHost>

Since we created the Let's Encrypt certs on the stock configuration, we can simply drop these configurations in and restart apache. Everything should just work. Just in case, check your configuration and debug accordingly before proceeding:

sudo apache2ctl configtest
sudo systemctl restart apache2

Make sure you've run ss -tulpn and that you are only listening on ports with services you intend and desire to be on this instance. Don't proceed if you have rogue services listening. Once that's verified, you can optionally add a firewall on top for extra coverage:

sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 22
sudo ufw enable

Now, although this instance is only for family-based watching and viewing, not public, I still want to tweak mpm_event and fpm to be snappy. After all, I want the family's viewing experience to be as nice as possible. Indeed, the wifey BMWing about AirSonic Advanced being too clonky is why we are here, so let's make it shine!

Adjust mpm_event for 8 cores and 400 workers. Again, overkill, but it certainly won't hurt anything. Head over to nano /etc/apache2/mods-available/mpm_event.conf and adjust the settings to something like this:

StartServers 4
MinSpareThreads 25
MaxSpareThreads 75
ThreadLimit 64
ThreadsPerChild 25
MaxRequestWorkers 400
MaxConnectionsPerChild 0
ServerLimit 16

Again, I see lots of folks that still use apache pre-fork and, almost as bad, those that use mpm_event instead, but forget that it requires configuring to be usable by more than a handful of clients. Let's open up nano /etc/php/8.4/fpm/pool.d/www.conf and drop in some beefier settings to handle simultaneous requests better:

pm = dynamic
pm.max_children = 200
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 20
pm.max_requests = 1000
request_terminate_timeout = 300s

Let's test the configuration and then restart the services:

sudo php-fpm8.4 -t
sudo systemctl restart php8.4-fpm apache2

The rest of the setup is conducted on the web panel. So, navigate to your isntance, e.g., https://gnulinux.media, set up your admin user, record your password, etc. Then, go in and add each of the Libraries in the dashboard. Once you've added them, save and scan them. In my case, I get all of my files to my main production server using Nextcloud. Once they are on the Nextcloud instance, I use rsync to mirror or place them in appropriate instances. Here's a script I run nightly on the Jellyfin server, so that it picks up media I add each day. Of course, you can also run it manually as needed. I only included a few example directories to make the point.

#!/bin/bash
#timer
DATE=`date +"%Y%m%d-%H:%M:%S"`
START0="$(date +%s)"
 
#begin script
touch /root/logs/sync-media.log
touch /tmp/sync-music.lock
echo "$(date): Starting music sync..." >> "$LOG"
# remove --delete because it will zap the cover art Jellyfin gets you
sudo rsync -ai --log-file=/root/logs/sync-media.log --chown=jellyfin:jellyfin root@server.com:/location/to/media/Flicks/* /srv/jellyfin/media/Flicks/
sudo rsync -ai --log-file=/root/logs/sync-media.log --chown=jellyfin:jellyfin root@server.com:/location/to/media/Classes/* /srv/jellyfin/media/Classes/
sudo rsync -ai --log-file=/root/logs/sync-media.log --chown=jellyfin:jellyfin root@server.com:/location/to/media/Concerts/* /srv/jellyfin/media/Concerts/
 
echo "$(date): Media sync completed." >> "$LOG"
#end script
 
# end timer
END0="$(date +%s)"
DURATION0=$[ ${END0} - ${START0} ]
MINUTES0=$[ ${DURATION0} / 60 ]
 
#email report
sed -i "1s/^/Jonathan, at $(date), $(hostname -f) synced its media libraries with x.club in ${DURATION0} sec. or ${MINUTES0} min.\n/" /root/logs/sync-media.log
#only appends, not prepends
/usr/bin/mail -s "[sync-music-success]-$(hostname -f)-$(date)" notices@haacksnetworking.org < /root/logs/sync-media.log
 
rm /root/logs/sync-media.log
rm /tmp/sync-music.lock

Again, this script runs daily and/or manually as needed. Sometimes I setup custom jails for fail2ban, but I am leaving this one stock for now. I think regular fail2ban with apache's and ssh's jails setup should be sufficient. The rest of Jellyfin can now be configured from your web panel.

oemb1905 2025/11/30 18:33