17 minute read

Introduction/Background

How it all started

I have always been a fan of self-hosting services. For years I have been running a Jellyfin server on my Windows laptop, we well as a VPN and DNS server on my Raspberry Pi. However, I have always wanted to have a dedicated server to host all my services, something that is powerful enough to run multiple services at once (in Docker containers) and low power enough to leave running in the basement in my house.

I have been looking at various options for a while, including a Raspberry Pi 4, a NUC, and a HP elitedesk 800 g4 that many tech YouTubers have raved about. However, the price tag for those were just a little too high for a broke college kid like me.

This is until recently my good friend/roommate GHigh (whose name is also George) broke his laptop screen and decided to gift me his laptop. Although the screen is broken, it is still a very powerful laptop with an 11th Gen Intel i7 and Iris Xe Graphics. Since both of us are named George, I am calling it “Gserve”.

       _,met$$$$$gg.          george@gserver
    ,g$$$$$$$$$$$$$$$P.       --------------
  ,g$$P"     """Y$$.".        OS: Debian GNU/Linux 12 (bookworm) x86_64
 ,$$P'              `$$$.     Host: HP ENVY x360 2-in-1 Laptop 15-ew0xxx
',$$P       ,ggs.     `$$b:   Kernel: 6.1.0-21-amd64
`d$$'     ,$P"'   .    $$$    Uptime: 5 mins
 $$P      d$'     ,    $$P    Packages: 1639 (dpkg)
 $$:      $$.   -    ,d$$'    Shell: bash 5.2.15
 $$;      Y$b._   _,d$P'      Resolution: 1920x1080
 Y$$.    `.`"Y$$$$P"'         Terminal: /dev/pts/0
 `$$b      "-.__              CPU: 12th Gen Intel i7-1255U (12) @ 4.700GHz
  `Y$$                        GPU: Intel Alder Lake-UP3 GT2 [Iris Xe Graphics]
   `Y$$.                      Memory: 1118MiB / 15704MiB
     `$$b.
       `Y$$b.
          `"Y$b._
              `"""

With the 12th Gen Intel i7 as well as Iris Xe Graphics, this laptop should be more than capable of running Jellyfin with multiple transcodes, as well as other services, which is my main goal for this server. It should also be pretty power efficient since it’s a laptop with recent hardware. In fact, I measured it to consume around 6 watts at idle (when unplugged, later I found it uses 9 watts when plugged in), and can jump up to 20 watts when transcoding my 4K 10bit HDR copy of Dune Part 2 from HEVC to H264 at around 131 fps.

12:50 PM -> george at -bash in ~/gserve
$ awk '{print $1*1e-6 " W"}' /sys/class/power_supply/BAT1/power_now
6.142 W

12:53 PM -> george at -bash in ~
$ awk '{print $1*1e-6 " W"}' /sys/class/power_supply/BAT1/power_now
20.125 W

The Plan

Currently I would like to run the following services on this server:

  • Jellyfin
  • QBittorrent
  • Arr suite (Sonarr, Radarr, Lidarr)

I would also like to have the following features:

  • Reverse Proxy
  • Dynamic DNS
  • Monitoring

I’ll be using Docker to run all these services, as it is the easiest way to manage multiple services on a single machine. As for operating system, I’ll be using Debian as the host OS, as it is the most stable and secure OS for servers.

Initial Setup

Installing Debian

I was able to find a detailed instruction on how to install Debian on the laptop from a guide from WikiHow. I followed it step by step and it worked perfectly. Looking back, one thing I might want to change is I wouldn’t have install the Gnome desktop environment, as I won’t be using it much and would like to save some resources. But it wouldn’t be a big deal since I can always just disable DE from starting up.

Basic Networking

Being a Networking Guru (many thanks to Prof. Kermani), I know that it is important to assign a static IP address to the server, since that’s how computers talk to each other. The command is simple:

sudo ip addr add <IP Address>/<Subnet Mask> dev <Interface>

Now I can just SSH into the server using the static IP address assigned and work on it remotely.

Turning off the Screen

Since this is a server, I don’t need the screen to be on all the time. I found a very good guide from Daniel Wayne Armstrong’s website, where he gave a few options. I tried the “vbetool” command in the “2.5 Turn off backlight” section, but it throws an error Real mode call failed and I am not able to turn off the screen. However, his suggestion to edit the /etc/systemd/logind.conf file and change all the #HandleLidSwitch=suspend to HandleLidSwitch=ignore worked for my purpose, which allowed me to close the lid to turn off the screen without suspending the system.

Preventing the Laptop from Sleeping

Another issue arose when I was editing this document, where the SSH session would disconnect after receiving a broadcast message Broadcast message... The system will suspend now!. This is annoying because I would like the server to be always on regardless of user activity. Luckily, after hours of Googling, I was able to find a solution from the Debian Forum, where they suggested to edit the /etc/systemd/logind.conf file and change the #AllowSuspend=yes to AllowSuspend=no. After this change the server hasn’t gone to sleep since.

Showing battery status

On the top right of Gnome Desktop there is a battery status icon as well as remaining battery percentage. Since I won’t be using the desktop environment, I would like to have a way to check the battery status from the terminal. I found an article from the Linux Journal that allows me to check the battery status using the upower command. I made a file called “battery” that allow me to check the battery status with a single command.

12:31 AM -> george at -bash in ~/useful
$ cat battery
#!/usr/bin/env sh
upower -i $(upower -e | grep BAT) | grep --color=never -E "state|to\ full|to\ empty|percentage"

12:38 AM -> george at -bash in ~/useful
$ ./battery
    state:               charging
    time to full:        3.2 minutes
    percentage:          99%

Mounting External Drives

Since I would be using this laptop with an external HDD and a high capacity SD card, I would like to have them mounted automatically when the server boots up so I don’t need to keep running the mount command every time I reboot the server. I found a useful guide from TechHut that involves the following steps:

  1. Find the UUID of the drive using the blkid command:
    01:04 PM -> george at -bash in ~/useful
    $ sudo blkid
    [sudo] password for george:
    /dev/nvme0n1p3: UUID="16edab1c-35b4-4957-bf1e-da41dcb9c15b" TYPE="swap" PARTUUID="bdae2779-6300-4503-a3ec-d21e252dad44"
    /dev/nvme0n1p1: UUID="2387-6FBE" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="3633eba1-e35d-4039-bc85-1b3dc53d743f"
    /dev/nvme0n1p2: UUID="37abe9ff-b0b2-4d8e-bd98-1ad18debe13a" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="6cf2d96f-752b-4c4a-af78-5fbb13575fe8"
    /dev/mmcblk0p1: UUID="9C33-6BBD" BLOCK_SIZE="512" TYPE="exfat"
    /dev/sda1: LABEL="easystore" BLOCK_SIZE="512" UUID="90DE784DDE782D9A" TYPE="ntfs" PARTLABEL="easystore" PARTUUID="b967632f-3af3-4419-b611-d56063b5b215"
    

As we can see, my external HDD is /dev/sda1 and the UUID is 90DE784DDE782D9A, while my high capacity SD card is /dev/mmcblk0p1 and the UUID is 9C33-6BBD. It also shows the filesystem type, which is NTFS for the HDD and exFAT for the SD card.

  1. Create a directory to mount the drive to:
    01:04 PM -> george at -bash in ~/useful
    $ sudo mkdir /mnt/harddrive /mnt/sdcard
    
  2. Edit the /etc/fstab file to include the following lines: (replace the <tab> with an actual tab)
    UUID=90DE784DDE782D9A<tab>/mnt/harddrive<tab>ntfs<tab>defaults<tab>0<tab>0
    UUID=9C33-6BBD<tab>/mnt/sdcard<tab>exfat<tab>defaults<tab>0<tab>0
    
  3. Run the mount -a command to mount the drives without rebooting the server.

Now the drives should be mounted automatically every time the server boots up.

Setting up Jellyfin and QbitTorrent in Docker

Setting up Docker

I followed the official Docker installation guide to install Docker on the server. I also installed Docker Compose using the official guide. Once that’s done, I was able to run the following command to check if Docker is installed correctly:

11:20 PM -> george at -bash in ~
$ docker --version
Docker version 26.1.3, build b72abbb

11:20 PM -> george at -bash in ~
$ docker compose

Usage:  docker compose [OPTIONS] COMMAND

Define and run multi-container applications with Docker

Setting up Jellyfin

Jellyfin is a FOSS (Free and Open Source Software) media server that allows you to host your own media library. It has an interface that looks similar to Netflix, but I can fill it with “my own” media and access it from my phone, laptop, or TV. I can also set up multiple accounts to share with friends and family.

Jellyfin Home Page

I have been running Jellyfin on my Windows Laptop for a while, but it requires my laptop to be on and connected to the external drives that I use (an external HDD and a high capacity SD card), which is not ideal. Now that I have this laptop, I can easily leave Jellyfin running on it and external drives plugged in.

At first, I tried to use the official Jellyfin Docker image, but doing hardware accelerated transcoding always throws a “playback error” in the client. So I found another image from linuxserver.io that fixes this issue. I created an entry in my docker-compose file for this server that looks something like this:

jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000 # default user id, use `id $USER` to find your user id
      - PGID=1000 # default group id, use `id $USER` to find your group id
      - TZ=America/Detroit  # set your timezone
      - JELLYFIN_PublishedServerUrl=<server IP>:8096  # optional
    # Optional, for hardware accelerated transcoding
    devices:
      - /dev/dri:/dev/dri
    volumes:
      - <config location>:/config
      - <drive location>:/data/drive
    ports:
      - 8096:8096
      - 8920:8920 #optional
      - 7359:7359/udp #optional
      - 1900:1900/udp #optional
    restart: unless-stopped

Then we can run the following command to start the Jellyfin container:

sudo docker-compose up -d

Now I can access Jellyfin by going to http://<server IP>:<local port> in my browser. I can also access it from my phone by downloading the Jellyfin app and entering the same address.

Setting up QBittorrent

QBittorren is a software that allows you to download torrents from the internet. While I don’t encourage downloading copyrighted material, there are also many legal torrents that you can download using QBittorrent (where else would you get your completely legal copies of Linux ISOs?). However, it is also important to protect your privacy while downloading torrents, as your IP address is visible to everyone in the torrent swarm.

I used to be a big fan of Mullvad VPN, as they are a privacy focused VPN provider that allows you to pay with Cryptocurrency or even cash, and they are known to not keep logs of user activities. However, they have really fell off in the recent years by disabling port forwarding to “prevent abuse”. Port forwarding is such a useful feature for torrenting, as it allows you to connect to peers that are behind a NAT instead of just peers that are port forwarded. Not only would this speed up your download speed, but it would also give you better upload ratio, which is important for private trackers. Therefore, I have switched over to AirVPN, which has an interface that doesn’t look as good as Mullvad, but they are also well known to be privacy focused and has port forwarding. However, for this guide, any VPN provider that supports OpenVPN should work.

Since I don’t want to run the VPN on the host machine, I found a Docker Image by Markus McNugen that has QBittorrent and OpenVPN built in. I created an entry in my docker-compose file for this server that looks something like this:

qbittorrentvpn:
    image: markusmcnugen/qbittorrentvpn
    container_name: qbittorrentvpn
    privileged: true
    volumes:
      - <path to config dir>:/config
      - <path to download dir>:/downloads
    environment:
      - VPN_ENABLED=yes
      - LAN_NETWORK=192.168.1.0/24  # replace with your LAN network
      - NAME_SERVERS=8.8.8.8,1.1.1.1
      - INCOMING_PORT_ENV=8999
      - WEBUI_PORT_ENV=8080
      - PUID=1000
      - PGID=1000
    ports:
      - 8080:8080
      - 8999:8999
      - 8999:8999/udp

Then I also need the OpenVPN configuration file from AirVPN. Fortunately, AirVPN has a page where you can generate the OpenVPN configuration file for your account. I downloaded the file and placed it in the config directory that I specified in the docker-compose file.

01:25 PM -> george at -bash in ~/gserve/qbit_vpn_config/openvpn
$ l
AirVPN_America_UDP-443-Entry3.ovpn*

After that, I used docker-compose up -d to start the container. Now I can access the QBittorrent WebUI by going to http://<server IP>:<local port> in my browser and login with the default username admin and password adminadmin. After logging in, it should look something like this:

QBittorrent WebUI

It is also important to go to Tools -> Options -> Web UI and change the username and password to something more secure, as well as going to Tools -> Options -> Advanced -> Network Interface and change it to the IP address of the VPN interface, which is usually tun0. This would ensure that all the traffic from QBittorrent goes through the VPN, and prevent any unwanted letters from your ISP.

If your VPN provider supports port forwarding, you can also go to Tools -> Options -> Connection and set the port to the port that is forwarded by the VPN provider. This would bind the port to the VPN interface and allow you to connect to more peers in the torrent swarm. Note that this port does not need to be forwarded on your router (explained in the next section), as it is handled by OpenVPN.

Setting up Port Forwarding and Dynamic DNS

Setting up Port Forwarding

This is the most important step in setting up a server, as it allows the server to be accessed from the outside world. To explain it simply, when a computer is connected to a network through Wifi or Ethernet, it is connected to a private network, in which it is given an IP address that is used by other computers in the private network to talk to it. However, for a computer to communicate with a computer outside of the private network, it uses a technology called NAT (network address translation) where the router “translates” the private IP addresses of the computers in the private network to its own public IP address and does all the communication on behalf of the computers. However, this means computers outside the private network cannot initiate communication with computers inside the private network. This is where port forwarding comes in. By setting up port forwarding on the router, we can tell the router to forward all the traffic coming to a specific port on the public IP address to a specific computer in the private network.

However, there is no specific instruction I can give you guys, as every router is different. I would recommend you to look up the model of your router and search for “port forwarding” in the manual. Some popular ports to forward are ports like 22 (SSH), 80 (HTTP), 443 (HTTPS), and 8096 (Jellyfin). A common practice is to use non-standard ports for services like SSH to avoid brute force attacks, but some might also argue that port scanning can easily find the non-standard ports so it doesn’t really matter. I just did it for the extra peace of mind.

Setting up Dynamic DNS

The problem with port forwarding is that the public IP address of the router can change, especially if you are using a residential internet connection. This means that the public IP address that you set up the port forwarding for might not be the same in a few days. This is where Dynamic DNS comes in. Dynamic DNS is a service that allows you to assign a domain name to your public IP address, and it will automatically update the domain name to point to the new public IP address when it changes.

For this, I used DuckDNS, a free Dynamic DNS service hosted on AWS. I created an account and set up a subdomain under duckdns.org, and followed the instructions to set up the DuckDNS client on the server. They have a web page that shows you exactly what commands to run on the server to have it update the domain name every 5 minutes, which I followed on my server.

Now, I can access my server by going to http://<subdomain>.duckdns.org:<port> in my browser, and the domain name will always point to the server even if the public IP address changes.

Setting up Reverse Proxy

While the server can now be accessed from the outside world, it is not very secure to expose those HTTP services directly to the internet as it is not encrypted. A possible solution to this is to get a SSL certificate for the domain name and set up HTTPS on the server. However, this is a lot of work to set up for each service, as we need to get a certificate for each service and set up HTTPS for each service. This is where a reverse proxy comes in. A reverse proxy is a server that sits in front of the other servers and forwards the requests to the correct server based on the domain name. This allows us to have a single SSL certificate for the reverse proxy server and have all the services behind the reverse proxy server to be encrypted.

For this, I followed a guide from Wolfgang’s Blog that shows how to set up a reverse proxy with DuckDNS and ACME DNS-01. Wolfgang is a German YouTuber that makes videos about self-hosting and privacy, and I have been a follower of his channel for a while, he is one of the reasons I got into self-hosting.

His guide shows how to set up the reverse proxy for private IP addresses, but it should still work for public IP addresses. I created a docker-compose.yml file that looks something like this:

proxymanager:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      # These ports are in format <host-port>:<container-port>
      - '80:80' # Public HTTP Port
      - '443:443' # Public HTTPS Port
      - '81:81' # Admin Web Port
      # Add any other Stream port you want to expose
      # - '21:21' # FTP

    # Uncomment the next line if you uncomment anything in the section
    # environment:
      # Uncomment this if you want to change the location of
      # the SQLite DB file within the container
      # DB_SQLITE_FILE: "/data/database.sqlite"

      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: 'true'

    volumes:
      - ./nginx-proxy-manager/data:/data
      - ./nginx-proxy-manager/letsencrypt:/etc/letsencrypt

This will create an Nginx reverse proxy server that listens on ports 80 and 443 for HTTP and HTTPS traffic, and port 81 for the admin web interface. I can access the admin web interface by going to http://<server IP>:81 in my browser.

Then I followed the guide to set up the SSL certificate for the domain name, and set up the reverse proxy for the services that I have. I will spare you the details, but the end result is that I can access all the services by going to https://<sub-subdomain>.<subdomain>.duckdns.org in my browser, and all the traffic is encrypted.

subdomain

Now I can access my services anywhere in the world with an internet connection, and know that the traffic is encrypted and secure.

Ending Notes

This is the end of the guide on how I set up my personal server. I hope you find it useful and informative. I will continue to update this document as I add more services to the server. If you have any questions or suggestions, feel free to leave a comment below. Thanks for reading!