Jellyfin Forum
Containerized Jellyfin media server - openSUSE MicroOS - Printable Version

+- Jellyfin Forum (https://forum.jellyfin.org)
+-- Forum: Support (https://forum.jellyfin.org/f-support)
+--- Forum: Guides, Walkthroughs & Tutorials (https://forum.jellyfin.org/f-guides-walkthroughs-tutorials)
+--- Thread: Containerized Jellyfin media server - openSUSE MicroOS (/t-containerized-jellyfin-media-server-opensuse-microos)



Containerized Jellyfin media server - openSUSE MicroOS - Mirronth - 2024-07-23

Hello there,

I hope my guide will help a few of you to configure your own Jellyfin media server or give you some ideas to improve. 
Please keep in mind, I'm just an end-user, this took me a month to figure out and put it together. I'm open to suggestions. Enjoy.


OS Installation

I prefer openSUSE MicroOS, so if you use something else, you can skip this part.

Enable firewall at the installation settings (optional).
If you install MicroOS Container Host, as I do, add ‘policycoreutils-python-utils’ during the installation process, which is required for ‘semanage’. If you install, for example the Gnome (RC) version, this package is included by default.


SSH & firewall rules

If you'd like to ssh directly to the root user, allow it. You have to create the following file and it’ll be used from now on if you restart the “sshd” service.
vi /etc/ssh/sshd_config.d/auth.conf
#Put this into the file
PermitRootLogin yes
systemctl restart sshd

If not, because of security, you can create a user which you'll use for this.
useradd <user>
passwd <user>


First, enable the SSH port. If you didn’t enable the firewall, skip this step.
firewall-cmd --permanent --add-service=ssh

These ports should be enabled too, because the containers we install need these ports to function properly. If you didn’t enable the firewall, skip this step.
firewall-cmd --zone=public --add-port=8096/tcp --permanent
firewall-cmd --zone=public --add-port=443/tcp --permanent
firewall-cmd --reload

Keep in mind, port 443 is only required, if you are planning to use Caddy for the automatic TLS for HTTPS. In this case, you have to port forward this port on your router too. This is only necessary, if you plan to reach Jellyfin outside of your local network.


Change the start of the unprivileged port number (You can skip this step if you just want to use Jellyfin locally)

Setting net.ipv4.ip_unprivileged_port_start=443 allows non-root processes to bind to ports starting from 443. Caddy sets up the necessary configuration to handle these challenges on port 443. If port 443 is not open or accessible from the internet, the Let's Encrypt validation will fail, and Caddy won't be able to obtain or renew TLS certificates automatically.
vi /etc/sysctl.conf
#Put this into the file
net.ipv4.ip_unprivileged_port_start=443


SELinux booleans (If you don't use SELinux, skip this)

You must turn on the container_manage_cgroup boolean to run containers with systemd.
setsebool -P container_manage_cgroup on

Enabling this boolean may grant the necessary SELinux permissions for containers to access and use DRI devices on the system. It is necessary for Hardware Acceleration.
setsebool -P container_use_dri_devices 1


User creation/configuration

Lets create and set up the user for the containers.
Creating a dedicated user for the containers, without default home directory. If you are fine with the default, '-M' is not needed.
useradd -M <user>

Add the user to the 'render' and ‘video’ group.
usermod -a -G render,video <user>

Create a directory to the user. If you created the user to have a default home directory, this step is not needed.
mkdir -p /var/my_data/home/<user>

Change the owner and group of the directory to the user. If you created the user to have a default home directory, this step is not needed.
chown <user>:<user> /var/my_data/home/<user>

Only the user has full access. This is up to you.
chmod 700 /var/my_data/home/<user>

Make the directory to the user's home directory. If you created the user to have a default home directory, this step is not needed.
usermod -d /var/my_data/home/<user> <user>

The enable-linger option ensures that the user's session persists across reboots and allows background tasks to continue running.
loginctl enable-linger $(id -u <user>)


Directories for the containers

Create the necessary directories for our containers. Do it with the root user.
sudo -iu <user> mkdir -p /var/my_data/home/<user>/jellyfin/{config,cache,media}
sudo -iu <user> mkdir -p /var/my_data/home/<user>/jellyfin/media/{subdir1,subdir2,subdir3}

You can create subdirectories if you want, like ‘/media/family_videos’, /media/<etc>.

Caddy and DuckDNS are only required, if you plan to reach Jellyfin outside of your network in a secure way. If you only plan to use it locally, you can skip the Caddy and DuckDNS configurations.
sudo -iu <user> mkdir -p /var/my_data/home/<user>/duckdns
sudo -iu <user> mkdir -p /var/my_data/home/<user>/caddy/caddy_data


DuckDNS configuration (If you only plan to use Jellyfin locally, skip this step)

Configure DuckDNS to automatically update your dynamic IP. For more details https://www.duckdns.org/install.jsp
cd /var/my_data/home/<user>/duckdns
vi duckdns.sh
#Put this into the file
echo url="https://www.duckdns.org/update?domains=<your_domain>&token=<your_token_from_duckdns>&ip=" | curl -k -o ~/duckdns/duck.log -K -

Make it executable. Try it out ./duckdns.sh, cat duck.log, if it is OK, then it is working fine.
chmod +x duckdns.sh


Caddyfile configuration (If you only plan to use Jellyfin locally, skip this step)

Here, I prefer to switch to our user. I use DuckDNS for my setup, so modify it according to your needs. The IP, is the internal IP assigned to my server.
su - <user>
cd /var/my_data/home/<user>/caddy
vi Caddyfile
#Put this into the file
<your_domain>.duckdns.org {
(tab)reverse_proxy <your_internal_IP>:8096
(tab)tls {
(tab)dns duckdns <your_token_from_duckdns>
(tab)}
}


DuckDNS systemd service/timer (If you only plan to use Jellyfin locally, skip this step)

Switch back to root user. Make the duckdns.sh to run every 5 minutes. First we need a systemd timer.
vi /etc/systemd/system/duckdns.timer
#Put his into the file
[Unit]
Description=Run DuckDNS script every 5 minutes

[Timer]
OnCalendar=*:0/5
Persistent=true

[Install]
WantedBy=timers.target

Then we need a systemd service. It will be run by the user, but is fine via root too. It is not needed to be user specific.
vi /etc/systemd/system/duckdns.service
#Put this into the file
[Unit]
Description=Update DuckDNS dynamic DNS

[Service]
Type=oneshot
User=<user>
ExecStart=/bin/bash -c '/var/my_data//home/<user>/duckdns/duckdns.sh'

[Install]
WantedBy=multi-user.target

systemctl enable duckdns.timer
systemctl enable duckdns.service


Fstab configuration


Auto mount your external drive. If you do not have/need one, skip these steps. For more details https://www.youtube.com/watch?v=LkwZZIsY9uE
To get the name of the device that you are looking for.
fdisk -l

To get the UUID for the device and also the format type.
blkid

Here, I give a permanent mount point for my external drive which, if you plan to use it like I do, will be the ‘/var/my_data/home/<user>/jellyfin/media’.
vi /etc/fstab
#Put this into the file
UUID=<UUID>(tab)/path/to/dir(tab)ext4(tab)defaults(tab)0(tab)0

This will change the ownership of your ‘path/to/dir’ so set it back.


Jellyfin and Caddy containers


Switch to your user.
Create the Jellyfin container. https://jellyfin.org/docs/general/installation/container/#podman

Some description:

’--label "io.containers.autoupdate=registry"’ Assigns a label to the container, indicating that it should be automatically updated from the registry.
’--publish <your_internal_IP>:8096:8096/tcp’ This publishes port 8096 on the host's IP address <your_internal_IP> to port 8096 in the container, allowing access to Jellyfin service.
’--network bridge’ Places the container on the default network bridge. You can use 'host' if you have performance issue with this, 'birdge' has higher CPU utilization but more secure.
’--user 0:0’ Gives the user root privilege inside the container.
’--group-add=$(getent group render | cut -d: -f3)’ This adds the group ID of the "render" group inside the container.
’--device /dev/dri/renderD128:/dev/dri/renderD128:rwm’ This makes the render device available inside the container with read, write, and mknod permissions.

IMPORTANT! Due to SELinux, be careful how you use this in the future. In the podman config, '/config:Z' means unshared, so only that specific container will be able to see its content where it belongs to. For shared you have to use lowercase 'z'. For the media, I used '--mount', so it is obvious at the end that those directories under media will be shared with other programs too. Otherwise, pointing other programs toward that directory, they won't be able to see it's content.

podman create --replace \
  --label "io.containers.autoupdate=registry" \
  --name jellyfin \
  --publish <your_internal_IP>:8096:8096/tcp \
  --network bridge \
  --user 0:0 \
  --group-add=$(getent group render | cut -d: -f3) \
  --device /dev/dri/renderD128:/dev/dri/renderD128:rwm \
  --volume /var/my_data/home/<user>/jellyfin/cache:/cache:Z \
  --volume /var/my_data/home/<user>/jellyfin/config:/config:Z \
  --mount type=bind,source=/path/to/dir,destination=/media,ro=true,relabel=shared \
  docker.io/jellyfin/jellyfin:latest


If you only plan to use Jellyfin locally, skip this step. Create the DuckDNS specific Caddy container. If you are not using DuckDNS, better to take a look at https://github.com/Sjord/caddy-modules, and pull what you need.

podman create --replace \
  --label "io.containers.autoupdate=registry" \
  --name caddy \
  --publish <your_internal_IP>:443:443/tcp \
  --network bridge \
  --user 0:0 \
  --volume /var/my_data/home/<user>/caddy/Caddyfile:/etc/caddy/Caddyfile:z \
  --volume /var/my_data/home/<user>/caddy/caddy_data:/data:Z \
  docker.io/serfriz/caddy-duckdns:latest


Apply SELinux container policy


Switch back to the root user (exit).
The first command sets up the file context specification based on an existing one, and the second command ensures that the contexts are applied and set to their default values recursively within the specified directory.
semanage fcontext -a -e /var/lib/containers /var/my_data/home/<user>/.local/share/containers
restorecon -R -F /var/my_data/home/<user>/.local/share/containers


Systemd services for the containers


Systemd configuration, so the containers will start automatically at system boot, with the dedicated user.
vi /etc/systemd/system/jellyfin.service
#Put this into the file
[Unit]
Description=Jellyfin container

[Service]
Restart=always
User=<user>
ExecStartPre=/bin/sleep 30
ExecStart=/usr/bin/podman start -a jellyfin
ExecStop=/usr/bin/podman stop -t 2 jellyfin

[Install]
WantedBy=default.target


vi /etc/systemd/system/caddy.service
#Put this into the file
[Unit]
Description=Caddy container

[Service]
Restart=always
User=<user>
ExecStart=/usr/bin/podman start -a caddy
ExecStop=/usr/bin/podman stop -t 2 caddy

[Install]
WantedBy=default.target

systemctl daemon-reload
systemctl enable jellyfin.service caddy.service

If the containers are not running, start them.
systemctl start jellyfin.service caddy.service


That's it. Hope I could help.

Mirronth

Edit: Typo


RE: Containerized Jellyfin media server - openSUSE MicroOS - TheDreadPirate - 2024-07-23

Very detailed guide. Thank you for spending so much time on this!


RE: Containerized Jellyfin media server - openSUSE MicroOS - Donovadrews - 2024-08-08

If you could elaborate, that would be fantastic, or maybe offer me a link to a detailed explanation of the approach.


RE: Containerized Jellyfin media server - openSUSE MicroOS - Mirronth - 2024-08-14

I wanted a reliable Jellyfin server for myself. MicroOS offers easy backup solution and the containerized Jellyfin can be easily recreated anytime.