Secure & Private Media Server

A step by step guide
Podman Containers

With the average smart phone user taking 7000+ (1500 by conservative estimates) photos each year, the need to manage those large number of pictures (and videos!) is more than ever. There are options by various service providers (amazon photos, google photos, etc.) to store and share that media, but if you are like me then you would want to keep your media on a privately and sovereign privately hosted cloud that you can manage, control. Neither your privacy, nor the quality of your photos/videos should be a comprising you should need to make. At the same time, access to your media should be super-secure, and also backed up so your don't lose it if your private cloud has a hiccup.

Whats different here?

When it comes to setting that up, there are quite a few blogs available already that offer setting up of media server, but none that I came across that could meet the security, sovereignty, and quality level I had wanted to use. Besides, they would often offer a half-baked solution instead of a full and comprehensive star-to-finish set of steps

Hence, I opted to develop my own set of steps and then document those steps in the form of this blog so that it offers a step by step step by step guide to others.

Lets set to goals

Media Server Application

The media server here will be setup such that:

  • Not resource hungry – able to run on a low end server
  • You are able to access it from anywhere, using credentials; on your (smart) TV, phone, or anywhere through the internet etc.
  • Photos can be played back as a slideshow and are easily searchable
  • You are able to share specific content with different user accounts (separate for each friend, family member etc.)
  • You, and your friends/family are able to upload media to your server

Resource Security:

To ensure server is properly restricted:

  • I have opted to run it in rootless Podman container, without any ability to initiate internet connection
  • Restict its ability to reach other devices within your private network
  • Limit the CPU/Memory it has access to (again, thanks to use of Podman containers)

Access Security:

The server implementation shall ensuring that security is not overlooked. Even though the media server application you may choose is trustworthy and open-source it should be restrained and restricted as much as possible to prevent against any mischievous code that may exist in it. At the same time, access to it should be restricted , encrypted and robust.

To secure access to the media server, the blog walks you through the following security measures:

  • Hidden behind a time-and-community-hardened http/s server (such as Apache or Nginx)
  • Using HTTPS to encrypt traffic
  • Authenticate using credentials and 2FA with OTP
  • Use non standard ports to further mask out the server's existence
  • Use of Linux Firewall to allow only required ports to be accessible
  • At the host's level, use linux security features such as SELinux

Requirements

Hardware Requirement

You don't need a high-end server for this. In fact, an old PC that you may have put aside to be recycled may serve this purpose quite well. I have found it to work perfectly well on a 4 core (dual threaded) Intel i7-720QM processor from 2009! The system had 8G of RAM and a standard NIC. If using an old PC, the one component you would want to upgrade will be a SSD drive, and given that you will be storing a lot of photos/videos on it, it should preferably be 1TB or higher. A single disk is sufficient, and you don't need RAID; you can use a cloud disk for backup purposes. So in short, here is what you require in terms of hardware:

CPUMemoryDiskNIC
A decent 4 Core (or more) processor8G recommendedSSD, preferably 1TB or moreStandard 1G interface (or higher)

DNS Requirement

Just to access your media server from outside your home network, you don't necessariliy need a DNS name. However, to enable HTTPS, a DNS name becomes a hard requirement. There are a couple of different options that you can use here:

  • If you already have a domain name, you can simply point it to your home IP address. Even if you don't want to use your primary domain name, you can use a subdomain for this purpose, for example if you own "mydomain.net", then you can define "mymedia.mydomain.net" and point that to your public IP address
  • If you don't own a domain name, and don't want to buy one either, you can get a free DNS name. My preferred choice is no-ip, but other options such as DuckDNS etc. are also available. The drawback of using these free DNS providers is that you can't use a non-standard port for http/s to "hide" your media server and restricted to use of standard ports (i.e. 80 and 443)

Home Router Ports

External access to your private media server will require you to open up the respective port in your home router. Typically, these routers don't allow you to use different port numbers for "inside" and "outside" addresses. In this blog, the inside port being used is 8242 and 8343, and hence the respective ports will need to be opened in your home router's firewall settings, and mapped to the internal IP address of your media server

Lets get started...

Install the Host OS:

For the host operating system to run your media server, I will suggest to use a recent version of Red Hat Enterprise Linux (RHEL) or any of its downstream distro. I will use Rocky version 9.8 for purpose of this guide. You can use the "minimal" ISO to perform the installation.

Installation of the Linux environment is too basic to cover here, so I will assume you have it up and running. Connect the physical NIC card (WiFi can be used, but not recommended) and since you will have a DHCP server already running in your home network you should have IP connectivity on this server.

No need for GUI environment as you will not be running other applications on this server. While installing, use LVM or Logical Volumes for the disk as it will enable you to easily expand it later if you run into disk space limitations.

Install the required tools

Once Linux is installed, install the following applications on it:


sudo dnf update –y
sudo dnf install tree jq git podman httpd nginx epel-release mod_ssl policycoreutils-python-utils –y. 
# policycoreutils-python-utils for "semanage", 
# mod_ssl for https
# epel-release for snapd
sudo dnf install snapd –y
# snapd for http/s cert management
Lets run our media server:

I chose "JellyFin" as the media server to use. Its opensource,and meets all the requirements that I listed previously.

Create required directories

Lets create three directories that it requires:


mkdir -p {media,jellyfin/config,jellyfin/cache}
restorecon -rF {media,jellyfin/config,jellyfin/cache}

The names of the directories can be anything, the ones above are just the ones I choice. The purpose of each of these sub directories is as follows:

sub-directorypurpose
mediathis is where you will store all your photos and videos
jellyfin/configthis is where JellyFin will store its configuration data
jellyfin/cachethis is used by JellyFin for caching

At this point, our setup looks like the following:

...

Create restricted network

To meet the security requirements, however, I will choose to install Jellyfind in a container instead of running it directly on the host machine. To isolate it, I will define its own container network. To ensure that the container always runs when the system is rebooted, its best to define it as a user-leverl systemd servrice. Similarly, the network definition can also be defined as a systemd service.

Use the following command to define the restricted container network, calling it:


mkdir –p ~/.config/containers/systemd/
cat << EOF > ~/.config/containers/systemd/media.network
[Unit]
Description=Network for Media 

[Network]
NetworkName=media_net
NetworkDeleteOnStop=false
Internal=true
DisableDNS=false
EOF 

This configuration enforces the network to be internal, enables local DNS lookup for the containers attached to this network. For details of the parameters used, refer to this Podman documentation

What we have now is this:

...

Create container for media server

Now lets define the podman service unit to run Jellyfin as a container utilizing the above defined network:


cat << EOF > ~/.config/containers/systemd/jellyfin.container 
[Unit]
Description=Jellyfin Latest 
Wants=network-online.target
After=network-online.target

[Container]
ContainerName=jellyfin
Image=docker.io/jellyfin/jellyfin:latest
Network=media.network

# Port publishing
PublishPort=127.0.0.1:8199:8096

# Volume
Volume=/home/myuser/jellyfin/cache:/cache:Z
Volume=/home/myuser/jellyfin/config:/config:Z
Volume=/home/myuser/media:/media:z

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
EOF

For details of the parameters used, refer to this document. In a nutshell, this container uses the Jellyfin latest docker image, uses the isolated network, maps the container to listen to port 8199

Our server can now be visualized as this:

...

Run the media server as a service

The files created here, referred to as "Quadlet Files” now need to be enabled to run by systemd service. To achieve that, use the following set of commands:


systemctl --user daemon-reload
loginctl enable-linger myuser 
systemctl --user start media-network
systemctl --user start jellyfin

You can verify that the service is successfully running by using the followings:


$ systemctl --user status jellyfin.service 
● jellyfin.service - Jellyfin Latest
     Loaded: loaded (/home/myuser/.config/containers/systemd/jellyfin.container; generated)
     Active: active (running) since Wed 2026-09-09 17:43:37 EDT; 6 days ago
   Main PID: 819955 (conmon)
<>

Or check the podman container running using "podman ps" command

Enabling External Access to the Media Server

Configure HTTP access

Your server is up and ready at this point, but its not yet accsssible from outside your host. To make that possible, lets run a web server with reverse proxy to this container. This way, the only way to access this container and its GUI will be through the webserver; which will then enforce encryption as well as roboustness

I have chosen nginx for this purpose. Default installation was already done in previous step, lets go ahead and configure it for our use

Edit the file /etc/nginx/nginx.conf , and comment out all the sections in the "server" section. For example:


#    server {
#        listen       80;
#        listen       [::]:80;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#        location = /404.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#        location = /50x.html {
#        }
#    }

Now create your own configuration file for the virtual host, using the example below:


sudo cat << EOF > /etc/nginx/conf.d/media.conf

server {
    ## for jellyfin
    listen 8242;
    location / {
        proxy_pass http://127.0.0.1:8199/;
        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;
    }
}
EOF

Firewall and SELinux

As a final step, lets ensure that firewalld (running by default) and SELinux (enabled by default) allow access to the http (and HTTPS ports as well, in the future).

For the firewall, use the following set of commands:


sudo firewall-cmd --add-port=8242/tcp --add-port=8242/udp --add-port=8343/tcp --add-port=8343/udp --permanent 
sudo firewall-cmd --reload 

For SELinux, use the followings:


sudo semanage port -a -t http_port_t -p tcp 8242
sudo semanage port -a -t http_port_t -p tcp 8343
sudo setsebool -P httpd_enable_homedirs on
sudo setsebool -P httpd_can_network_connect 1

Starting web server

Lets start the nginx server:


sudo systemctl enable --now nginx

Ensure that its running using the following:


systemctl status nginx

Output should show "active (running)"

So now we have the following setup:

...
Secure External access using HTTPS

Lets now enable https. We can use letsencrypt, a freely avaialble service, for that. However, before we generate certificates, we need to ensure that domain name is set up and access to the server has been properly configured.

Setting up the domain and home internet access

I will assume that you are using your own domain name, and have already pointed it towards your home IP address. At this point, you should also open up the port (8242 and 8343 in our case) on the home router's firewall, and map these ports to the respective ports on your media server's IP address

Generating Certificate

To use letsencrypt to generate a signed certicate, the tool used is called "Certbot", whcih is installed using "snapd". snapd was previously installed in an earlier stape, but we just need to enable it at this point:


sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
# Wait a minute or so for the service to run, and then use the followings: 
sleep 60
sudo snap install core; sudo snap refresh core

Finally, install certbot using:


sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Now you can use the certbot command to generate and install the certificates for your domain name:


sudo certbot --nginx -d  your.domain.net

Configure Nginx for HTTPS

Certbot would not only create the certificates, but it will also create the required config for it. However, it assumes default https port (443) and as a security measure I would prefer to use a non-standard port. Hence, I will remove the auto created https entry in nginx configuration (you will find it in /etc/nginx/cond.d/media.conf) , and replace it with:


sudo cat << EOF >> /etc/nginx/media.conf
server {
    ## for jellyfin https (external)
    listen 8343 ssl;
    server_name mymedia.mydomain.net;
    ssl_certificate /etc/letsencrypt/live/mymedia.mydomain.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mymedia.mydomain.net/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8199/;
        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;
    }
}

Restart nginx a final time:


sudo systemctl restart nginx

Your media server is now ready and shall be accessible from anywhere remotely. Following is what this final setup looks like

...
Bringing up & Configuring the server

Accessing the serer for the first time

Now that your media server is ready, you can go ahead and access it. Note that if you are accessing it from your LAN, then access it on the non-https port and use the host's IP address. But if accessing from outside your network, then you should use https and point towards the https port and domain name that you had used

Bring up the server

When you reach the server, you are presented with a welcome screen like the following one. Here, you can give your server a preferred name and move to the next screen for the configuration options:

...

Configure user name

The next configuraiton screen will ask you to configure a username and password to use. This first user will also be an admin user for your server. Choose the password carefully because this media server is exposed to the internet and prone to attacks.

...

Configure Media

On this next screen, add media location and category. You can select the top level folder that we had configured ("/media") and give it a library name.

...

At this point, you can also go ahead and start copying your photos and videos to the ~/media/ directory of your host machine. I will recommend to use different folders for pics/videos from different events, and include the dates in the name of the folder. This will help you search much conveniently. For example, I use something like "2025_01_12_Red_Hat_One_Event" as folder name.

...

Disable remote access

In the subsequent steps, disable the "remote access" to the jellyfin server. We will still be accessing it remotely, but the server doesn't need to know that, and we woudn't want it to enable any such acess.

...

Finish the configuration

You are done with the basic configuration at this point. Proceed to Finish

...

The server will ask you to login again, using your newly created credentials. You can go ahead and view the dashboard and configure the server further if you want, or add more media etc.

You can continue to add more media files in the background. If any recently uploaded content doesn't show up in your library, you can use the "Scan All Libraries" option to import these contents (if not happening automatically already) into Jellyfin's database.

Tip

You can upload all your files and folders to the top level "~/media" directory, and make that folder part of your primary library in jellfin. However, if you want to limit some user to specific content, then create special library for that user and only share the sub-folders that are meant to be shared with them. You can always add/manage the libraries later as well.
Securing the media server

About jellyfin Plugins

Jellyfin offers various plugins, and though many of those are focused towards video/movie plauback, there are quite a few that are very meaningful to add to your home server as well.

To see the plugsins you have, or to add new ones, you can use the following sequence of steps to reach the plugins page:

...

The default installed plugins will show something like the followings:

...

To add new plugins, you can search the existing repository, or add new repository for specific plugins that you may want. However, one challenge you will face here is that the jellyfin instance is fully disconencted, and hence it can't download any plugin directly. The plugin we are primarily intereseted in is the Jellyfin Security plugin.

Adding Security Plugin

Since out jellyfin is disconnected, we somehow need to download a copy of the security plugin and copy that into our server's configuration. This section shows how you can achieve that.

Shortcut

You can optionally skip the steps in this section, and directly download a copy of the security plugin files from this location

The simplest way to download the plugin files is by running another (temporary) copy of jellyfin container, fully connected to the internet, download plugins that you desire, and then copy over the plugin files to your actual jellyfin instance, and remove that temporary (connected) instance. You can use the steps to download any plugin that you may want, but I will only demonstrate it for the security plugin that enables 2FA.

To start, lets use the following steps to create the temporary (connected) copy of Jellyfin, that is using the directory "/tmp/jfconfig" to store its configuration:


mkdir /tmp/jfconfig
podman run -d --rm --name temp -p 9999:8096 -v /tmp/jfconfig/:/config:Z docker.io/jellyfin/jellyfin:latest
sudo firewall-cmd --add-port=9999/tcp --permanent
sudo firewall-cmd --reload 

You can now access this Jellyfin intance, on your local network, using port 9999. For example, if your host's IP address is 192.168.1.140, then point your browser to http://192.168.1.140:9999

Using the previously described set of steps, configure this jellyfin server, and reach the "plugin" menu. Then choose the "Manage Repositories" option to add a new repository. As shown below, add this new repository: "https://raw.githubusercontent.com/ZL154/JellyfinSecurity/main/manifest.json"

...

Now on the main plugin page, you can find the "Jellyfin Security" plugin available, as can be seen here:

...

Go ahead and click on it, and then choose the "Install" option:

...

Once this plugin (and any other that you may want to download) are downloaded and installed, the job of the temporary instance is complete. You can now go ahead and delete this temporary instance of jellyfin using the following:


podman stop temp

Installing Security Plugin

So what did we achieve? The downloaded plugins are actually in this location now: "/tmp/jfplugin/plugin/". You can copy this into your "real" jellyfin's configs using the following command:

Tip

If you had donwloaded the plugin instead of installing it (therefore skipping the previous section), then just untar the downloaded file and copy its contents instead of the copy command shown below

 cp -r /tmp/plugins/* ~/jellyfin/config/plugins/

At this point, go ahead and restart your jellyfin for the plugin to be loaded. You can do that using the Jellyfin dashboard, but I prefer to do that using systemctl instead so that all the changes properly take effect:


systemctl --user restart jellyfin.service

Now with the plugin installed, when you login to your jellyfin instance, and go to the plugin section, you will see it listed there as shown here:

...

There are a number of security features this plugin provides. The minuimum ones that I would recommend are the ones showin in the following image as this will enable 2FA for all the users, disable email recovery, and skip 2FA for LAN requests (you will have to enter the appropriate LAN subnets in the field towards the bottom of this image:

...

You may also want to look at the other options such as password strength enforcement etc.

Backing up your media

Now that the server is ready and secured, there is one last requirement that is yet to be met, i.e. backing up your prestigious media files to a secure and safe loction. One simple way would have been use of RAID disk upfront, but that protects only against local disk failure. Most of us (if not all!) have a large amount of public cloud storage avaialable which they aren't fully utilizing. Your Gdrive, Onedrive, etc. may be offering you Terabytes that may be heavily underutilized. So I opted to use that spare space as backup for my local media. When using as a backup location and storing raw files on it, I don't risk loss of image/video quality, and yet utilize the space for free.

You may be thinking at this point, how do I backup your locally stored media files to a public cloud account? As always, I looked for possibly options available through the open source community and found the perfect solution offered by an open-source mature tool called rclone. You can intstall and run it directly on your host machine, but personally I always prefer to use containers to isolate and limit applications that don't need to run on the host. So I have built a simple container for rclone, and you can find it here. You don't need to build the container however, the steps below will directly pull from my public container registry.

Lets go ahead and perform this one time configuration for rclone:


mkdir ~/rclone
podman run -it --rm  --name rclone -v ~/rclone:/root/.config/rclone:Z quay.io/sfhassan/rclone rclone config

When you run the above commands, rclone will present to you an interactive menu for first time configuration, like the one shown here:


Trying to pull quay.io/sfhassan/rclone:latest...
Getting image source signatures
Copying blob 4523943475c4 skipped: already exists  
Copying blob 42ced0cd92d6 skipped: already exists  
Copying blob 2a6eae3b626b skipped: already exists  
Copying blob a778088874e2 skipped: already exists  
Copying config 9e80b63794 done   | 
Writing manifest to image destination
2026/09/17 22:34:38 NOTICE: Config file "/root/.config/rclone/rclone.conf" not found - using defaults
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
n/s/q> 

The configuration from this point onwards can vary depending on which cloud storage service you are using. I will demonstrate the steps used for Google-Drive in the snippets that follow:


No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
n/s/q> n

Enter name for new remote.
name> my_gdrive

Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
[[ SNIPPED ]]
23 / Google Cloud Storage (this is not Google Drive)
   \ (google cloud storage)
24 / Google Drive
   \ (drive)
25 / Google Photos
   \ (google photos)
[[ SNIPPED ]]
Storage> drive

The steps involved from this point onwards will vary based on the choice you make for the storage provider. Rclone's site documents the needed steps in a very detaled way, and I don't need to replicate those here. You would find those easy to follow in general.

Once this process is complete, a config file is saved by rclone into the directory you specified. So now you can simply run rclone everytime you would like your local media to be backed up on the remote storage location, for example:


podman run -it --rm  --name rclone -v ~/rclone:/root/.config/rclone:Z -v ~/media/:/media:ro quay.io/sfhassan/rclone rclone sync -P -L  /media my_gdrive:media/ 
All done!

Horray! you are all set and your personal, private, secure, and roboust media server should now be all good to go. I hope the steps were easy and simple to follow. Please share any feedback you may want to share via email

Bonus Content - uploading files!!

Now that you have the server environment built, its a simple extra step to add the capability for your users to upload their content from their devices as well - still keeping it password based, and secure. I am not going to cover this in this blog, but you can refer to my git page for a comprehensive set of steps on setting up "FileGator" (and opensource uploading tool) to this ecosystem that you have built for your media server