> ## Content Index
> Fetch the complete content index at: https://blog.dbotech.net/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to set up a media server with Jellyfin, Docker, and Cloudflare Tunnel
- URL: https://blog.dbotech.net/how-to-set-up-a-media-server/
- Published: 2026-09-22T11:05:40.000Z
- Updated: 2026-09-25T16:16:42.000Z
- Description: A complete beginner guide to installing Docker and Jellyfin, organizing a media library, and configuring remote access with Cloudflare Tunnel.
- Author: Derr
- Tags: Media Server, How-To, Jellyfin, Docker, Cloudflare

A spare PC, one storage drive, and an afternoon are enough for a useful media server. This guide installs Ubuntu Server, runs Jellyfin with Docker Compose, separates application data from your media, and adds remote access without forwarding a port on your router.

## What we are building

The server has four parts:

- A stable Linux host
- Docker Compose to run the services
- Jellyfin to stream your local media
- Cloudflare Tunnel so you can watch your media away from home

LAN and WAN clients reach the same Jellyfin server through separate paths.

![Diagram separating LAN playback from WAN playback. LAN media clients connect directly to Jellyfin, while remote clients connect through Cloudflare and the outbound cloudflared tunnel.](https://blog.dbotech.net/content/images/2026/09/media-server-overview-mnt.png)

*LAN and WAN clients reach the same Jellyfin server through separate paths.*

## What you need

- **A computer that can run Linux.** Preferably, you want an Intel chip with Intel Quick Sync.
- **A wired network connection, if possible.** You can technically use Wi-Fi, but you will make your life a lot easier if you just use Ethernet.
- **An unused IP address on your LAN.** You will also need your subnet, gateway, and DNS information.
- **Enough storage for the media you already own.** I would start with at least 4 TB.

I know... drive prices suck right now. Do some Googling and shop around for a good deal. ServerPartDeals has a great warranty and decent prices on used drives.

- **A Cloudflare account and a domain using Cloudflare DNS.**
- **Another computer that you can use to connect to the server over SSH.**

If you do not know what SSH is, read up on it before continuing. You do not need to become an expert, but you should understand how to connect to another computer and run commands remotely.

### A quick note about hardware

Jellyfin has a recommended hardware guide, but here is my personal experience.

I started with a ZimaBoard. You can definitely use one, but it will struggle with 4K media. My honest recommendation is to get on eBay or Facebook Marketplace and find a used mini PC. You can usually find them pretty cheap, and you are helping reduce e-waste at the same time.

Please do not use a Raspberry Pi.

Yes, Raspberry Pis are great, but you are going to have a bad time running Jellyfin on one. Trust me.

And if you do not want to trust some random dude on the internet, here is what the Jellyfin team has to say:

> Most SBCs, including the Raspberry Pi and especially the Raspberry Pi 5, are too slow to provide an acceptable Jellyfin experience because they often lack proper support for hardware acceleration.  
>  
> If you really want to run Jellyfin on an SBC, consider models based on Rockchip RK3588 or RK3588S, Intel Core, or Intel 12th-generation N-series platforms.

## Okay, now what? How do I get this set up?

## 1\. Install Linux

I am using Ubuntu Server for this guide. Download the current Ubuntu Server LTS image, write it to a USB drive, and boot your media server from it.

During the installation:

- Give the server a name you will recognize, such as `media-server`.
- Install OpenSSH when the installer gives you the option. Doing this during setup saves you a few steps later.
- Create a normal administrator account. Do not log in as root for everyday use.
- Set up the static IP address during installation.

On the **Network connections** screen, select your wired interface and choose **Edit IPv4**. Change the IPv4 method from **Automatic (DHCP)** to **Manual**, then enter your subnet, static IP address, gateway, and DNS servers.

Trust me, doing this now is much easier than changing the address after you have already connected over SSH. Just make sure the address is outside your router's DHCP pool so it cannot be assigned to another device.

Every router is different, so if you do not know how to find the DHCP range, Google will be your best friend.

Once the installation is finished, connect to the server over SSH and install the available updates:

```bash
sudo apt update
sudo apt upgrade -y
sudo reboot

```

The SSH connection will close when the server reboots. Give it a minute, then reconnect using the static IP address you assigned during installation.

## 2\. Install Docker and Docker Compose

We are going to run Jellyfin and Cloudflare Tunnel in Docker containers. This keeps the applications separated from the rest of the operating system and makes the setup much easier to reproduce later.

Install Docker Engine and the Compose plugin from Docker's official repository.

First, add Docker's official GPG key:

```bash
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings

sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc

sudo chmod a+r /etc/apt/keyrings/docker.asc

```

Add the Docker repository:

```bash
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

```

Install Docker:

```bash
sudo apt update

sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

```

Confirm that Docker and Compose are working:

```bash
sudo systemctl status docker --no-pager
sudo docker run --rm hello-world
sudo docker compose version

```

If the `hello-world` container runs successfully and Compose prints a version number, you are ready to move on.

## 3\. Create the application and media folders

We are going to keep Jellyfin's configuration separate from the actual media files.

Create the folders:

```bash
sudo mkdir -p /opt/jellyfin/{config,cache}
sudo mkdir -p /mnt/media/{movies,shows,music}

sudo chown -R "$USER":"$USER" /opt/jellyfin /mnt/media

chmod 750 /opt/jellyfin
chmod 755 /mnt/media /mnt/media/{movies,shows,music}

cd /opt/jellyfin

```

The folders under `/opt/jellyfin` contain Jellyfin's settings and cache.

The folders under `/mnt/media` contain your movies, shows, and music.

If your media is stored on a NAS or external drive, mount it under `/mnt/media` before starting Jellyfin. Make sure the mount survives a reboot. Docker will happily start with an empty local folder if the real drive or network share is missing, which can make it look like your entire library vanished.

The Compose file will give Jellyfin read-only access to `/mnt/media`. Jellyfin can scan and play the files, but it cannot delete them.

![Diagram mapping Ubuntu host paths for Jellyfin configuration, cache, and media to their matching paths inside the Jellyfin container.](https://blog.dbotech.net/content/images/2026/09/media-paths-mnt.png)

*Host folders map to predictable paths inside the Jellyfin container.*

## 4\. Create the Cloudflare Tunnel

Log in to Cloudflare and open:

**Networking → Tunnels**

Create a remotely managed tunnel. Give it a recognizable name, such as `home-lab`, and select Docker as the environment.

Cloudflare will give you a tunnel token. Treat this token like a password. Anyone with it can connect another tunnel process to your Cloudflare account.

We are keeping the token in its own folder and mounting it into the container as a Docker Compose secret.

Run:

```bash
sudo mkdir -p /opt/cloudflared/secrets
sudo chown -R "$USER":"$USER" /opt/cloudflared
chmod 700 /opt/cloudflared/secrets

nano /opt/cloudflared/secrets/cloudflare_tunnel_token

```

Paste the tunnel token into the file, then save and close it.

Do not add quotes or anything else. The file should contain only the token. Also, please do not upload this file to GitHub or paste the token into Discord. Again, treat it like a password.

Protect the token:

```bash
chmod 600 /opt/cloudflared/secrets/cloudflare_tunnel_token

```

### A quick warning about Cloudflare and video streaming

A Cloudflare Tunnel gives you a secure way to reach the server, but that does not mean every Cloudflare plan allows unlimited video delivery. Read Cloudflare's current video-delivery policy before pushing your entire 4K library through it.

## 5\. Create the Compose files

We are going to use two separate Compose files: one for Jellyfin and one for Cloudflare Tunnel.

I know two files sounds like more work, but each file stays simple, and you can restart or update one service without messing with the other. You also do not need to create or manage any custom Docker networks.

### Create the Jellyfin Compose file

Go to the Jellyfin folder:

```bash
cd /opt/jellyfin
nano compose.yaml

```

Paste the following YAML:

```yaml
services:
  jellyfin:
    image: jellyfin/jellyfin:12.1
    container_name: jellyfin
    ports:
      - "8096:8096"
      - "7359:7359/udp"
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /mnt/media:/media:ro
    restart: unless-stopped

```

Save the file and have Docker Compose check it:

```bash
sudo docker compose config --quiet

```

No output is good. It means Docker did not find a problem with the YAML.

Pull the Jellyfin image and start the container:

```bash
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps

```

Jellyfin should now show as running.

You can check its logs with:

```bash
sudo docker compose logs --tail=100 jellyfin

```

### Create the Cloudflare Compose file

Go to the Cloudflare Tunnel folder:

```bash
cd /opt/cloudflared
nano compose.yaml

```

Paste the following YAML:

```yaml
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    network_mode: host
    command:
      - tunnel
      - --no-autoupdate
      - run
      - --token-file
      - /run/secrets/cloudflare_tunnel_token
    secrets:
      - cloudflare_tunnel_token
    restart: unless-stopped

secrets:
  cloudflare_tunnel_token:
    file: ./secrets/cloudflare_tunnel_token

```

The `network_mode: host` line allows `cloudflared` to reach Jellyfin through Ubuntu's network. You do not need to create a shared Docker network or connect the two Compose projects manually.

Check the Compose file:

```bash
sudo docker compose config --quiet

```

Pull the image and start the tunnel:

```bash
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps

```

Check the tunnel logs:

```bash
sudo docker compose logs --tail=100 cloudflared

```

You should see the tunnel connect to Cloudflare.

### Update the Cloudflare service URL

Because `cloudflared` is using the host network, the published application route in Cloudflare needs to point to:

```text
http://localhost:8096

```

Do not use `http://jellyfin:8096`. That address only works when both containers share the same Docker network, and we are deliberately avoiding that setup.

Once the route is saved, Jellyfin should be available through the hostname you configured, such as:

```text
https://media.example.com

```

## 6\. Let's set up Jellyfin!

From another device on the same network, open:

```text
http://SERVER_LAN_IP:8096

```

Replace `SERVER_LAN_IP` with the static IP address you assigned earlier.

Create the Jellyfin administrator account using a long, unique password.

Add the libraries one at a time:

- **Movies:** `/media/movies`
- **Shows:** `/media/shows`
- **Music:** `/media/music`

Use the matching library type instead of putting everything into one giant mixed folder. Jellyfin handles movies, television episodes, and music differently.

Once you finish the setup wizard, you should start seeing your media appear. If you have a large library, give Jellyfin time to finish scanning it.

Once the scan is finished, try playing something. You should be able to play your media!

## 7\. Name your media so Jellyfin can identify it

Jellyfin relies heavily on folder and file names when matching your media.

For movies, use one folder per movie and include the release year:

```text
/mnt/media/movies/
└── Example Movie (2024)/
    └── Example Movie (2024).mkv

```

For television shows, use a folder for the show, separate season folders, and `SxxExx` episode numbers:

```text
/mnt/media/shows/
└── Example Show (2023)/
    ├── Season 01/
    │   ├── Example Show S01E01.mkv
    │   └── Example Show S01E02.mkv
    └── Season 02/
        └── Example Show S02E01.mkv

```

Yes, this is boring.

It also prevents a lot of incorrect posters, merged shows, and missing episodes. Take a few minutes to name everything correctly now instead of spending hours fixing the metadata later.

> Side note: If you have A LOT! of media do not do this manually. Radarr and Sonarr can rename your media. 

## 8\. Test the Cloudflare hostname

Return to **Networking → Tunnels** in Cloudflare.

The tunnel should show a healthy connector. Open the hostname you configured:

```text
https://media.example.com

```

The Jellyfin login page should load.

If you want to double-check remote access, use your phone and disable Wi-Fi. Navigate to your Jellyfin hostname using the mobile connection.

If it works, BOOM! LOOK AT YOU GO, NERD!

Do not forward TCP port `8096` or UDP port `7359` to the public Internet. The tunnel handles remote access, and port `7359` is only used for local discovery.

## 9\. Add hardware transcoding only if you need it

Direct play is always the easier option. Jellyfin sends the original file to the client without converting it.

Transcoding happens when the client cannot handle the video's codec, audio format, subtitles, container, or bitrate. It requires much more processing power.

I am not going to go into much detail about transcoding here. There is a lot of information, and it can become very confusing, very fast. I would 100% recommend reading Jellyfin's documentation on hardware acceleration before changing anything.

Get normal playback working first. Then add hardware acceleration if you actually need it.

## 10\. Back up the parts that matter

The containers and downloaded images are replaceable. Your configuration, tunnel token, and media are not.

Back up:

- `/opt/jellyfin/compose.yaml`
- `/opt/jellyfin/config`
- `/opt/cloudflared/compose.yaml`
- `/opt/cloudflared/secrets/cloudflare_tunnel_token`
- Your media library under `/mnt/media`

You do not need to back up `/opt/jellyfin/cache`. Jellyfin can rebuild it.

For a simple configuration backup, stop Jellyfin and create an archive:

```bash
cd /opt/jellyfin
sudo docker compose stop

cd /opt
sudo tar -czf "media-server-config-$(date +%Y-%m-%d).tar.gz" \
  jellyfin/compose.yaml \
  jellyfin/config \
  cloudflared/compose.yaml \
  cloudflared/secrets

cd /opt/jellyfin
sudo docker compose start

```

Copy the archive to another computer, external drive, NAS, or backup service.

A second copy on the same disk is not a backup. RAID is also not a backup. I know someone will still argue about this, but a failed drive does not care about your technical definition of redundancy.

## 11\. Update without rebuilding the server

One of the best parts of using containers is that you do not need to reinstall the entire server every time Jellyfin or `cloudflared` releases an update.

Before updating anything, make a backup. I know it is tempting to skip this part because “it is just a container,” but container updates can still include database migrations and other changes that are not easy to undo.

### Update Jellyfin

Go to the Jellyfin folder:

```bash
cd /opt/jellyfin

```

Pull the latest image from the `12.1` release line and recreate the container:

```bash
sudo docker compose pull
sudo docker compose up -d

```

Make sure Jellyfin came back:

```bash
sudo docker compose ps
sudo docker compose logs --since=10m jellyfin

```

Open Jellyfin and play something before moving on. A container showing as running does not always mean the application inside it is actually working.

### Update Cloudflare Tunnel

Go to the `cloudflared` folder:

```bash
cd /opt/cloudflared

```

Pull the newest image and recreate the container:

```bash
sudo docker compose pull
sudo docker compose up -d

```

Check its status and logs:

```bash
sudo docker compose ps
sudo docker compose logs --since=10m cloudflared

```

You should see the tunnel reconnect to Cloudflare.

Once you have confirmed that Jellyfin and the tunnel are working, you can remove old unused images:

```bash
sudo docker image prune

```

Docker will ask you to confirm before deleting anything.

Do not blindly schedule automatic major-version upgrades. Containers make updating easier, but they do not make every update safe. Read the release notes, make a backup, update one service at a time, and confirm that it works.

Trust me, spending five minutes checking an update is much better than spending your entire evening trying to rebuild a broken Jellyfin database.

## Stop here before making it complicated

At this point, you have:

- A Linux media server with a static IP address
- Jellyfin running in Docker
- A read-only media library
- Persistent Jellyfin configuration
- Local playback on your LAN
- Remote access through Cloudflare Tunnel
- Separate Compose files for Jellyfin and `cloudflared`
- A backup and update process

That is a complete media server.

You do not need Sonarr, Radarr, a request system, live TV, ten dashboards, or a full server rack to start watching your media.

Those things can be useful, and we will get to some of them later. For now, play a few movies, test your devices, and make sure the basic setup is stable.

It is much easier to troubleshoot two working containers than fifteen containers you installed in one afternoon because some random dude on the internet called it the “ultimate media stack.”

Keep it simple, add one thing at a time, and enjoy your media server.