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