Skip to main content

Jellyfin From Scratch: How to Build a Complete Media Server on Linux Mint (Docker, Nginx, and Let’s Encrypt)

Start from a clean OS and end with your own secure, self-hosted streaming platform — complete with HTTPS access, remote streaming, and full media organization.

🧠 Introduction

Jellyfin is a free, open-source media server that lets you organize and stream your movies, shows, and music to any device. Think of it as your personal Netflix — except you control everything.

In this guide, you’ll start from a clean Linux Mint installation and finish with a fully working Jellyfin server running inside Docker, served securely through Nginx with Let’s Encrypt SSL certificates, protected by Fail2ban, and accessible anywhere through a DuckDNS domain.

This tutorial uses CPU-only transcoding (no GPU acceleration) and follows current Docker Compose v2 standards. This means, if want to use your GPU to do all the heave lifting, I ain't gunna show you that here but, you can still use this tutorial to get your server up and running then, configure your GPU later.


🧰 Prerequisites

  • A clean install of Linux Mint (based on Ubuntu 20.04 or later)

  • Sudo privileges

  • Vim - Text editor (or which ever you prefer)
  • An active internet connection

  • A DuckDNS account and domain (e.g., myserver.duckdns.org)

  • Some media files stored locally or on a mounted drive


🧱 Step 1 — System Preparation

Start by updating your system and installing a few required packages.

sudo apt update && sudo apt upgrade -y

...give it a minute to do its thang.

sudo apt install -y ca-certificates curl gnupg lsb-release ufw fail2ban

.........This might take another quick minute. If you can any prompts, say "yes" or "y".

Enable the firewall and allow only essential services: (run each line separately)

sudo ufw default deny incoming 
sudo ufw default allow outgoing 
sudo ufw allow OpenSSH 
sudo ufw allow 80,443/tcp 
sudo ufw enable

Check status:

sudo ufw status

You should see SSH, HTTP, and HTTPS allowed. Did it work?


🐳 Step 2 — Install Docker and Docker Compose

Install Docker Engine and the Compose plugin using official repositories: These are to separate programs that work in tandem. "Docker" and "Docker Compose"

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ 
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ 
https://download.docker.com/linux/ubuntu \ 
$(lsb_release -cs) stable" | \ 
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Enable and start Docker: 
sudo systemctl enable docker
sudo systemctl start docker

Verify installation:

docker --version 
docker compose version

They should both tell you what version of Docker & Docker Compose you have.


📂 Step 3 — Create Directory Structure

Where do you want your Jellyfin to live? For this, we’ll keep Jellyfin and related configs in /opt/jellyfin.

sudo chown -R $USER:$USER /opt/jellyfin
sudo mkdir -p /opt/jellyfin/{config,cache,media,nginx,certbot}
Where is your media going to be stored? Mount your media drive to /opt/jellyfin/media. Or, if you already have a local address, you can put it there.
We are not going over how to mount a drive in this tutorial so, if you don't know how to do that part, go learn now, and come back. This example may not make sense to you.
Example:
sudo mkdir /mnt/media 
sudo mount /dev/sdX1 /mnt/media 
sudo ln -s /mnt/media /opt/jellyfin/media

Or, you could mount directly to the directory:

sudo mount /dev/sdX1 /opt/jellyfin/media

You need this permanent so, go do that in fstab.


⚙️ Step 4 — Create the Docker Compose File

Create the file:
This is a .yml file. Don't know the language? That's OK. I gotchew. 

sudo vim /opt/jellyfin/docker-compose.yml

Paste this configuration:
above, we made a bunch of directories. config, cache, media, nginx, certbot
Make sure that the directories in here match the ones you made. 

Press "i" to edit the text. Paste this in:

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1001
      - PGID=1001
      - TZ=America/Los_Angeles
    volumes:
      - ./home/user/jellyfin/config:/config
      - ./home/user/jellyfin/cache:/cache
      - ./home/user/jellyfin/media:/media
    ports:
      - 8096:8098:8096
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/www:/var/www/certbot
      - ./certbot/conf:/etc/letsencrypt
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - jellyfin
    restart: unless-stopped

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - ./certbot/www:/var/www/certbot
      - ./certbot/conf:/etc/letsencrypt
    entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done"
    restart: unless-stopped

Save and exit 
Esc
: 
wq
enter


🌐 Step 5 — Configure Nginx Reverse Proxy

Create a new Nginx configuration file:

mkdir -p /opt/jellyfin/nginx/conf.d
sudo vim /opt/jellyfin/nginx/conf.d/jellyfin.conf
Paste the following:
server {
        listen 80;
    server_name yourdomain.duckdns.org;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }Your.customesite.com;
            location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name yourdomain.duckdns.org;

    ssl_certificate /etc/letsencrypt/live/yourdomain.duckdns.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.duckdns.org/privkey.pem;

    location / {
        proxy_pass http://jellyfin:8096;
                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;
                proxy_http_version 1.1;
                proxy_set_header   Upgrade    $http_upgrade;
                proxy_set_header   Connection "upgrade";
                proxy_pass http://localhost:8096; #add your own port number if its not this
        }

    #place holder for certpot later. It will maintain your SSL cert for you.
    # you can delete this line and the one above it once you get the certbot running.
  
}
🔧 Replace yourdomain.duckdns.orgYour.customesite.com  with your actual DuckDNS (or other custom) domain.
Save and exit 
Esc
: 
wq
enter

🔑 Step 6 — Get Your SSL Certificate with DuckDNSyour domain and Certbot

First, make sure your DuckDNS domain is pointed to your public IP. If you haven't already, go to your domain dashboard and make sure.

Then,Go runhere to see how to get a temporaryan container to issue yourautomated SSL certificate:

cert. 

docker compose run --rm \ -v ./certbot/www:/var/www/certbot \ -v ./certbot/conf:/etc/letsencrypt \ certbot/certbot certonly --webroot \ --webroot-path=/var/www/certbot \ -d yourdomain.duckdns.org \ --agree-tos --email youremail@example.com --no-eff-email

When complete, you’ll have valid SSL certificates inside /opt/jellyfin/certbot/conf/live/.

***need to check out***  
*in the .yml file, is "./nginx/conf.d:" or "./certbot/www" a real location in the local drive?
*see if the nginx configuration is correct.