With streaming services becoming more fragmented, getting access to all your favorite content has become almost as inconvenient and expensive as it was in the days of cable packages.

Even if you happen to find your favorite content on a streaming platform, there’s no guarantee it will be around forever. A single discontinued licensing contract could mean losing access to countless pieces of media!

For these reasons, I have built up a fairly large music collection on my hard drive. I recently realized I needed a proper backup solution for my music and other important files.

I figured while I was at it, I would make my music collection streamable, because there’s lots of stuff Spotify and others don’t have. Then I figured since I was hosting music, I might as well start building a collection of movies and television for when I inevitably tire of paying for Netflix and Hulu.

Here’s a breakdown of the server I built:

PCPartPicker Part List

I went cheap for this build, because all it needs to do is reliably store media files. I gave it a decent CPU and 8GB of memory so streaming this media to a couple clients should be no problem.

Software Stack⌗

This diagram should give an overview of how our software talks to each other. Basically;

All programs are run in docker for portability and convenience

Sonarr watches usenet for TV shows I want

Couchpotato watches usenet for movies I want

SABnzbd downloads the NZBs found by Sonarr and Couchpotato

Jellyfin serves these media files with a pretty interface

Traefik generates SSL certs for the desired containers

Step 1: Setting up Docker⌗

In this tutorial I will assume you are a docker novice. If you already have some familiarity with docker, you can probably skip to the next section.

Since I am running my machine with Ubuntu Server, I am going to install the necessary packages with the following command:

sudo apt install docker.io docker-compose

docker is a program that runs applications in containers. These containers share the kernelspace with the host machine, but isolate their own dependencies and runtime environments. This is great for application security and portability.

docker-compose is a functionality within docker that allows a series of containers to be preconfigured, so they can all be launched, maintained, and updated easily and conveniently.

I set up my “programs” folder with a docker-compose.yaml file, along with subfolders for every application to store configurations and metadata.

Step 2: Setting up Traefik⌗

Traefik is a reverse proxy that integrates with Let’sEncrypt to dynamically provide SSL certificates to running applications. It will be able to securely direct HTTP requests to our server to the correct container.

If you aren’t planning on using these services outside your home network, it’s safe to skip this step. If you own a domain and wish to access your media anywhere, read on.

Traefik needs two files to work correctly. The Traefik configuration file, traefik.toml , and the configuration defining the container behaivior in docker-compose.yaml . Let’s take a look at that first.

version: '3' services: reverse-proxy: image: traefik:latest restart: always container_name: traefik ports: - 80:80 - 443:443 - 8081:8081 expose: - 8080 networks: - traefik environment: - DO_AUTH_TOKEN = <or another DNS provider if you use one> volumes: - /var/run/docker.sock:/var/run/docker.sock - ./Traefik/traefik.toml:/traefik.toml - ./Traefik/acme.json:/acme.json labels: - "traefik.backend=traefik" - "traefik.docker.network=traefik" - "traefik.enable=false" - "traefik.frontend.rule=Host:Host:monitor.mydomain.com" - "traefik.port=8080" - "traefik.frontend.headers.forceSTSHeader=true" - "traefik.frontend.headers.STSSeconds=315360000" - "traefik.frontend.headers.STSIncludeSubdomains=true" - "traefik.frontend.headers.STSPreload=true"

Some things to take away:

We need to bind ports 80 and 443 to receive HTTP/S traffic. These ports must be forwarded to your server from your router.

and to receive HTTP/S traffic. These ports must be forwarded to your server from your router. We mount our docker node to the traefik container so it can see our other containers

We mount traefik.toml and acme.json , the file used to store our SSL configurations.

and , the file used to store our SSL configurations. traefik.enable is set to false because I don’t need to use it outside my network.

Then we have to define some more settings in traefik.toml

defaultEntryPoints = [ "http" , "https" ] logLevel = "DEBUG" [ entryPoints ] [ entryPoints . dashboard ] address = ":8081" # I changed from 8080 because SAB uses it [ entryPoints . http ] address = ":80" [ entryPoints . http . redirect ] entryPoint = "https" [ entryPoints . https ] address = ":443" [ entryPoints . https . tls ] [ api ] entrypoint = "dashboard" [ acme ] email = "me@mydomain.com" storage = "acme.json" onHostRule = true entryPoint = "https" [ acme . dnsChallenge ] provider = "digitalocean" delayBeforeCheck = 0 [[ acme . domains ]] main = "*.mydomain.com" [ docker ] domain = "mydomain.com" watch = true network = "traefik"

The entrypoints section forces HTTPS and enables the dashboard on locahlhost:8081 .

. The acme section defines the settings for Let’sEncrypt, which may need to be tweaked for your setup.

The above file must be created:

touch acme.json && chmod 600 acme.json

To test out what we have so far, go to the root of your docker directory and run docker-compose up . Traefik was the hardest for me to configure, so read the output carefully for errors. If everything went OK, we should see our dashboard at localhost:8081 .

Step 3: Docker Configurations⌗

The other applications in this setup are mostly controlled through web interfaces. That means that after the container is up and running, you can change the settings via your web browser just how you like them.

I will go over the basic steps to get things running, but after that it’s up to you!

Here is the full docker compose file for my current setup

version: '3' services: reverse-proxy: image: traefik:latest restart: always container_name: traefik ports: - 80:80 - 443:443 - 8081:8081 expose: - 8080 networks: - traefik environment: - DO_AUTH_TOKEN = volumes: - /var/run/docker.sock:/var/run/docker.sock - ./Traefik/traefik.toml:/traefik.toml - ./Traefik/acme.json:/acme.json labels: - "traefik.backend=traefik" - "traefik.docker.network=traefik" - "traefik.enable=false" - "traefik.frontend.rule=Host:Host:monitor.gideonwolfe.com" - "traefik.port=8080" - "traefik.frontend.headers.forceSTSHeader=true" - "traefik.frontend.headers.STSSeconds=315360000" - "traefik.frontend.headers.STSIncludeSubdomains=true" - "traefik.frontend.headers.STSPreload=true" jellyfin: image: linuxserver/jellyfin container_name: jellyfin environment: - PUID = 1000 - PGID = 1000 - TZ = America/Los_Angeles volumes: - /home/gideon/Data/Programs/Docker/Jellyfin/ProgramData/:/config - /home/gideon/Data/Media/TV/:/data/tvshows - /home/gideon/Data/Media/Movies/:/data/movies - /home/gideon/Data/Music/:/data/music ports: - 8096:8096 labels: - "traefik.enable=true" - "traefik.port=8096" - "traefik.frontend.rule=Host:server.gideonwolfe.com" - "traefik.backend=JellyFin" - "traefik.frontend.entryPoints=https" - "traefik.frontend.headers.forceSTSHeader=true" - "traefik.frontend.headers.STSSeconds=315360000" - "traefik.frontend.headers.STSIncludeSubdomains=true" - "traefik.frontend.headers.STSPreload=true" - "traefik.docker.network=traefik" networks: - traefik restart: unless-stopped couchpotato: image: linuxserver/couchpotato container_name: couchpotato environment: - PUID = 1000 - PGID = 1000 - TZ = America/Los_Angeles - UMASK_SET = 022 volumes: - /home/gideon/Data/Programs/Docker/CouchPotato/config:/config - /home/gideon/Data/Programs/Docker/CouchPotato/Downloads:/downloads - /home/gideon/Data/Media/Movies:/movies # Where movies end up ports: - 5050:5050 restart: unless-stopped labels: - "traefik.enable=false" sabnzbd: image: linuxserver/sabnzbd container_name: sabnzbd environment: - PUID = 1000 - PGID = 1000 - TZ = America/Los_Angeles volumes: - /home/gideon/Data/Programs/Docker/Sabnzbd/config:/config - /home/gideon/Data/Media/:/downloads ports: - 8080:8080 - 9090:9090 restart: unless-stopped labels: - "traefik.enable=false" sonarr: image: linuxserver/sonarr container_name: sonarr environment: - PUID = 1000 - PGID = 1000 - TZ = America/Los_Angeles - UMASK_SET = 022 #optional volumes: - /home/gideon/Data/Programs/Docker/Sonarr/config:/config - /home/gideon/Data/Media/TV:/tv # Final folder where shows end up - /home/gideon/Data/Media/:/downloads # Same as SAB downloads ports: - 8989:8989 restart: unless-stopped labels: - "traefik.enable=false" networks: traefik: external: true internal:

Each service level represents a containerized docker application.

level represents a containerized docker application. The image level tells docker where to pull the image from.

level tells docker where to pull the image from. The environment level allows you to pass variables into the container.

level allows you to pass variables into the container. The volumes level mounts directories from the host into docker containers. This allows for persistant settings and access to media locations.

level mounts directories from the host into docker containers. This allows for persistant settings and access to media locations. ports defines the port to access the services

defines the port to access the services labels allow us to define additional settings, such as disabling SSL certs with traefik.

I would recommend creating a seperate docker-compose.yaml for each sevice, and testing them ony by one before combining them into a master compose file.

Step 4: Web configuration⌗

Jellyfin: localhost:8096 As long as your desired media is mounted in docker-compose.yaml , You can use the dashboard page to set up libraries with these folders. Piece of cake.

SABnzbd: localhost:8080 You need to connect SAB with a usenet provider using your account credentials and API key sent by your provider. This is done in the “Servers” settings tab. Set up the Categories tab to sort downloads into appropriate folders. For example, tv goes to ./TV , movies to ./Movies . These puts them in the folders Jellyfin looks in.

Sonarr: localhost:8989 Allow Sonarr to rename episodes under ‘Media Management’ Under the “Indexers” tab, enter the url and API key from any indexers you have accounts for. Sign up for as many as possible for maximum success finding media. Under “Download Client” tab, add SAB. You will need the local IP address of your SAB server as well as the API key found in the “General” section of the SAB settings. Optionally enable metadata fetching

CouchPotato: localhost:5050 Add your indexers in the “Searchers” tab Add SAB in the “Downloaders” tab Allow Movie Renaming



After all this is set up, we should be able to test it with some media.

Try finding a single TV episode through Sonarr using the magnifying glass. This should be sent to SABnzbd to download It should be in your media directory TV/Series/Season #/Episode Refresh Jellyfin and view the media

Once this pipeline is working correctly, add all the shows you want, and set up monitors so new episodes are automatically downloaded.

For CouchPotato, try adding a movie and seeing if the same behavior happens. Are your movies showing up in Jellyfin?

In this guide, I detailed the steps for setting up an end to end automated pipeline for downloading and streaming your favorite media. Although the initial setup can be tricky at times, the portability and convenience offered by docker makes this setup awesome!

Containers and services can easily be added to the configuration, such as Syncthing or Nextcloud for file backups and sharing.