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
  • 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
    volumes:
      - ./config:/config
      - ./cache:/cache
      - ./media:/media
    ports:
      - 8096: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

 

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