Tutorial 44: Homelab Media Server — Complete Docker Stack on Repurposed Laptop
Transform an old laptop into a powerful self-hosted media server running Jellyfin, the *Arr suite, qBittorrent, AdGuard Home, Nextcloud, and 15+ services — all managed through Docker Compose with Intel QSV hardware transcoding, automated media management, and remote access via Tailscale.
Difficulty: Beginner-Intermediate Reading time: ~60 minutes Prerequisites: A laptop or mini PC with an Intel CPU (6th gen+ for QSV), 16GB+ RAM, Ubuntu 24.04 installed Technologies: Docker, Jellyfin, Radarr, Sonarr, Prowlarr, qBittorrent, AdGuard, Nginx Proxy Manager, Tailscale, Tdarr, Nextcloud, Portainer OS: Ubuntu 24.04 LTS
Table of Contents
- Introduction — Why Self-Host a Media Server
- Hardware Overview & Storage Strategy
- Ubuntu Installation & System Configuration
- Environment Configuration — Centralized .env File
- Management Stack — Portainer, Tailscale, Watchtower
- Network & Security — AdGuard Home & Nginx Proxy Manager
- Download Stack — qBittorrent
- *Arr Media Automation Suite
- Jellyfin Media Server
- Nextcloud — Self-Hosted Cloud Storage
- Media Processing — Tdarr & Ollama
- Monitoring — Uptime Kuma
- Integration & Automation — Connecting Everything Together
- Backup & Disk Protection
- Dashboard — Landing Page with Service Links
- Troubleshooting — Common Issues and Fixes
- Conclusion
1. Introduction — Why Self-Host a Media Server
The Case for Self-Hosting
Streaming services keep raising prices, splitting content across platforms, and removing shows without warning. A self-hosted media server gives you:
- Complete control over your library — no content disappearing overnight
- No monthly fees — one-time hardware cost, then it runs forever
- Privacy — your viewing habits stay on your network
- Hardware transcoding — stream to any device, any format, automatically
- Automated media management — request a movie and it appears in your library, with subtitles, ready to watch on any device
- Network-wide ad blocking — DNS-level filtering for every device in your home
- Self-hosted cloud storage — replace Google Drive/iCloud with Nextcloud
- Remote access — watch your content from anywhere via Tailscale VPN
Why a Laptop?
Repurposing an old laptop is one of the best homelab decisions you can make:
| Advantage | Details |
|---|---|
| Built-in UPS | Battery survives short power outages — no separate UPS needed |
| Low power | 15-20W idle (vs 60-100W for a desktop/tower server) |
| Silent | Laptop fans are quiet, often fanless at idle |
| Compact | Sits on a shelf, no rack needed |
| Built-in display | Useful for initial setup, then run headless |
| Intel QSV | Most Intel laptops support Quick Sync Video for hardware transcoding |
An Intel i5 (11th gen or newer) with 16-32GB RAM and an NVMe SSD is the sweet spot. The integrated Intel Iris Xe GPU provides excellent hardware transcoding — H.264, H.265/HEVC, VP9, and AV1 decode, all in hardware.
What We Are Building
By the end of this tutorial, you will have a fully automated media server with this architecture:
User requests movie Radarr searches indexers qBittorrent downloads
via Jellyseerr --> via Prowlarr --> to NVMe SSD
|
v
Download completes
|
v
Radarr imports movie qBittorrent seeds to
to media library <-- ratio 1.0, auto-removes
|
v
Jellyfin detects new movie Bazarr auto-downloads
in library --> subtitles (.srt/.ass)
|
v
Tdarr (optional) transcodes
to H.265 using Intel QSV
|
v
Movie available for playback
on all devices (TV, phone, web)
Service Overview — Port Map
| Port | Service | Purpose | Access Level |
|---|---|---|---|
| 8096 | Jellyfin | Media streaming server | LAN + Tailscale |
| 5055 | Jellyseerr | Request portal (Netflix-like UI) | LAN + Tailscale |
| 8088 | Nextcloud | Self-hosted cloud storage | LAN + Tailscale |
| 8080 | qBittorrent | Torrent download client | LAN only |
| 8989 | Sonarr | TV show automation | LAN only |
| 7878 | Radarr | Movie automation | LAN only |
| 8686 | Lidarr | Music automation | LAN only |
| 9696 | Prowlarr | Indexer manager | LAN only |
| 6767 | Bazarr | Automatic subtitles | LAN only |
| 53 | AdGuard Home | DNS-level ad blocking | LAN |
| 8053 | AdGuard Home Admin | Ad blocker management UI | LAN only |
| 81 | Nginx Proxy Manager | Reverse proxy admin | LAN only |
| 80/443 | NPM (proxy) | Public reverse proxy | WAN (if configured) |
| 9000 | Portainer | Docker management GUI | LAN only |
| 3001 | Uptime Kuma | Service health monitoring | LAN only |
| 8265 | Tdarr | Automated transcoding | LAN only |
| 11434 | Ollama | Local AI/LLM API | LAN only |
| 8888 | Dashboard | Landing page with service links | LAN + Tailscale |
2. Hardware Overview & Storage Strategy
Recommended Hardware Specs
| Spec | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | Intel i5 6th gen | Intel i5 11th gen+ | Must have Intel Quick Sync (QSV) |
| GPU | Intel UHD 630 | Intel Iris Xe | Iris Xe adds AV1 decode, better HEVC |
| RAM | 16GB DDR4 | 32GB DDR4 | 16GB is tight with Nextcloud + Ollama |
| Boot Drive | 256GB SSD | 500GB NVMe SSD | OS + Docker + configs + downloads |
| Media Drive | - | 500GB+ SSD or HDD | Separate drive for media library |
| TDP | - | 28W (PL1) | ~15-20W idle |
Laptop-Specific Considerations
- Lid close: Must disable sleep-on-lid-close (the server runs headless 24/7)
- Battery: Built-in battery acts as a free UPS (survives short power outages)
- Network: WiFi works but Ethernet is recommended for media streaming reliability
- Screen/keyboard: Useful for initial setup, then run headless via SSH
Storage Strategy — Dual-Drive Setup
The ideal setup uses two drives: an NVMe SSD for the OS, Docker, and temporary downloads, and a second SSD (or HDD) for the media library.
NVMe SSD (boot drive) — OS, Docker, downloads, configs
├── / OS + Docker (~20GB)
├── /opt/appdata Container configs + databases (~5-10GB)
├── /opt/docker Docker compose files + scripts
├── /mnt/media Media library root
│ ├── downloads/ Torrent downloads (temporary storage)
│ ├── movies/ -> BIND MOUNT to /mnt/ssd/movies (second drive)
│ ├── tv/ TV shows
│ ├── music/ Music
│ └── books/ Books
└── /var/lib/docker Container images + volumes (~10-20GB)
Second SSD/HDD (/dev/sda1)
└── /mnt/ssd Mounted via fstab (by UUID)
└── movies/ Movie library, bind-mounted to /mnt/media/movies
Key design decision: The second drive is bind-mounted to /mnt/media/movies via fstab. This makes the second drive completely transparent to all containers -- Jellyfin, Radarr, Tdarr, and Bazarr all see /mnt/media/movies as if it were a local directory. No Docker volume changes needed when adding or replacing drives.
3. Ubuntu Installation & System Configuration
3.1 — Install Ubuntu 24.04 LTS
Download Ubuntu 24.04 LTS (Server or Desktop edition) and install it on the laptop. Desktop edition uses slightly more RAM but gives you a GUI for initial setup.
- Set hostname to something memorable (e.g.,
mediaserver) - Create your user account (this guide assumes UID/GID 1000)
- Connect to your network (Ethernet preferred)
3.2 — Update System
sudo apt update && sudo apt upgrade -y
sudo reboot
3.3 — Disable Lid Close Sleep
Edit /etc/systemd/logind.conf and set:
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
Then restart the login manager:
sudo systemctl restart systemd-logind
3.4 — Set Static IP (Recommended)
If your system uses NetworkManager (Ubuntu Desktop), use nmcli:
# For Ethernet:
sudo nmcli con add con-name "static-eth" ifname YOUR_ETH_INTERFACE type ethernet \
ipv4.method manual ipv4.addresses YOUR_IP/24 \
ipv4.gateway YOUR_GATEWAY ipv4.dns "1.1.1.1,8.8.8.8"
sudo nmcli con up static-eth
# For WiFi (adjust connection name from 'nmcli con show'):
sudo nmcli con modify "YOUR_WIFI_CONNECTION" \
ipv4.method manual ipv4.addresses YOUR_IP/24 \
ipv4.gateway YOUR_GATEWAY ipv4.dns "1.1.1.1,8.8.8.8"
# Verify
ip addr show
3.5 — Install Intel Quick Sync (QSV) Drivers
sudo apt install -y intel-media-va-driver vainfo mesa-va-drivers
Verify the installation:
vainfo
You should see output like:
VA-API version: 1.20
Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0
Confirm codec support (look for H.264, HEVC, VP9, AV1 entries). GPU devices should appear at /dev/dri/card0 (or card1) and /dev/dri/renderD128.
Note: The free
intel-media-va-driverpackage includes all commonly needed codecs. If you need extra codec profiles later, installintel-media-va-driver-non-freeinstead.
3.6 — Install Docker & Docker Compose
# Install Docker using the official convenience script
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group (avoids needing sudo for docker commands)
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect
# Verify:
docker --version
docker compose version
3.7 — Create Directory Structure
If your boot drive uses LVM (common with Ubuntu Server), expand it to use the full disk first:
# Expand LVM to use full disk (Ubuntu often allocates only ~100GB during install)
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
Create the media directories:
# Create all directories
sudo mkdir -p /opt/appdata /opt/docker/compose
sudo mkdir -p /mnt/media/{downloads/{torrents/{movies,tv,music},usenet/{movies,tv,music}},movies,tv,music,books,nextcloud}
# Set ownership to your user (UID/GID 1000)
sudo chown -R 1000:1000 /opt/appdata /opt/docker /mnt/media
3.8 — Configure Power Management for 24/7 Operation
# Disable sleep/suspend/hibernate
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
# Disable Wi-Fi power management (prevents random disconnects if using WiFi)
# Edit /etc/NetworkManager/conf.d/default-wifi-powersave-on.conf
# Change wifi.powersave = 2 (or 3) to:
# wifi.powersave = 0
3.9 — Set Up Automatic Security Updates
sudo apt install -y unattended-upgrades
For laptops, also install thermal management:
sudo apt install -y thermald
4. Environment Configuration — Centralized .env File
All containers share a single .env file for common settings like user IDs, timezone, and paths. This avoids hardcoding values in every docker run command.
Create /opt/docker/.env:
# User/Group IDs (run 'id' to check yours — usually 1000:1000)
PUID=1000
PGID=1000
TZ=YOUR_TIMEZONE
# Paths
MEDIA_ROOT=/mnt/media
APPDATA=/opt/appdata
# Gluetun VPN (optional — fill in if you want VPN-protected torrents)
# VPN_SERVICE_PROVIDER=mullvad
# VPN_TYPE=wireguard
# WIREGUARD_PRIVATE_KEY=your_key_here
# WIREGUARD_ADDRESSES=your_address_here
# SERVER_COUNTRIES=Netherlands
Replace YOUR_TIMEZONE with your timezone identifier (e.g., Europe/London, America/New_York, Europe/Berlin, Asia/Tokyo). This ensures all container logs and scheduled tasks use the correct time. All docker run commands below reference ${TZ} from this file, or you can hardcode the timezone directly in each command.
5. Management Stack — Portainer, Tailscale, Watchtower
5.1 — Portainer (Docker GUI)
Portainer gives you a web-based GUI for managing all your Docker containers, images, volumes, and networks.
docker run -d \
--name portainer \
--restart=always \
-p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/appdata/portainer:/data \
portainer/portainer-ce:latest
Access: http://YOUR_IP:9000
- Create an admin account on first visit
- Select "Local" environment
5.2 — Tailscale (Remote Access VPN)
Tailscale creates a secure mesh VPN so you can access your media server from anywhere (phone on 4G, laptop at a coffee shop, etc.) without exposing ports to the internet.
# Install Tailscale directly on the host (not Docker — more reliable on laptops)
curl -fsSL https://tailscale.com/install.sh | sh
# Start and authenticate
sudo tailscale up
# Follow the URL printed to authenticate with your Tailscale account
# (create free account at https://tailscale.com if you don't have one)
# Verify
tailscale status
Once connected, your server gets a Tailscale IP (100.x.x.x). You can access all services from anywhere using this IP.
Tip: Disable Tailscale's DNS management to avoid it hijacking your local DNS (especially important if you run AdGuard):
sudo tailscale set --accept-dns=false
5.3 — Watchtower (Automatic Container Updates)
Watchtower monitors your running containers and automatically pulls updated images, recreating containers with the same configuration.
docker run -d \
--name watchtower \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DOCKER_API_VERSION=1.53 \
-e TZ=Europe/London \
-e WATCHTOWER_CLEANUP=true \
-e WATCHTOWER_SCHEDULE="0 0 4 * * *" \
containrrr/watchtower:latest
This checks for container updates daily at 4:00 AM and removes old images after updating.
Note: If you see Docker API version errors, check your Docker API version with
docker version --format '{{.Server.APIVersion}}'and setDOCKER_API_VERSIONaccordingly.
6. Network & Security — AdGuard Home & Nginx Proxy Manager
6.1 — AdGuard Home (Network-Wide Ad Blocking)
AdGuard Home is a DNS-level ad blocker that works for every device on your network -- phones, smart TVs, IoT devices -- without installing anything on the devices themselves.
docker run -d \
--name adguardhome \
--restart=always \
-p 53:53/tcp \
-p 53:53/udp \
-p 3000:3000/tcp \
-p 8053:80/tcp \
-v /opt/appdata/adguardhome/work:/opt/adguardhome/work \
-v /opt/appdata/adguardhome/conf:/opt/adguardhome/conf \
adguard/adguardhome:latest
First-time setup at http://YOUR_IP:3000:
- Set the admin web interface to port 8053 (avoids conflicts with other services)
- Set DNS to port 53
- Create an admin username/password
- Add filter lists (recommended: AdGuard default + Steven Black's unified hosts)
After setup, the admin panel moves to: http://YOUR_IP:8053
Important: Set your router's DNS server to your media server's IP address so all devices on the network use AdGuard for DNS resolution.
6.2 — Nginx Proxy Manager (Reverse Proxy + SSL)
NPM lets you access services via clean domain names (e.g., jellyfin.yourdomain.com) instead of IP:port combinations, with automatic Let's Encrypt SSL certificates.
Note: If you have Nextcloud installed as a snap, remap its port first to free port 80 for NPM:
sudo snap set nextcloud ports.http=8088 && sudo snap restart nextcloud
docker run -d \
--name nginx-proxy-manager \
--restart=always \
-p 80:80 \
-p 443:443 \
-p 81:81 \
-v /opt/appdata/npm/data:/data \
-v /opt/appdata/npm/letsencrypt:/etc/letsencrypt \
jc21/nginx-proxy-manager:latest
Access: http://YOUR_IP:81
Default login: [email protected] / changeme (change immediately!)
You will configure proxy hosts here later for each service (e.g., jellyfin.yourdomain.com -> http://YOUR_IP:8096).
7. Download Stack — qBittorrent
qBittorrent is a torrent client with a web UI. We run it without a VPN here for simplicity, but you can add Gluetun (VPN container) later for privacy.
Privacy note: Without a VPN, your real IP is visible in torrent swarms. If you want VPN protection, deploy Gluetun and set
network_mode: "service:gluetun"on the qBittorrent container.
7.1 — Deploy qBittorrent
Create the compose file at /opt/docker/compose/downloads.yml:
services:
qbittorrent:
image: linuxserver/qbittorrent:latest
container_name: qbittorrent
ports:
- 8080:8080 # qBittorrent WebUI
- 6881:6881 # Torrent port (TCP)
- 6881:6881/udp
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- WEBUI_PORT=8080
volumes:
- /opt/appdata/qbittorrent:/config
- ${MEDIA_ROOT}/downloads/torrents:/downloads
restart: always
Deploy:
cd /opt/docker
docker compose -f compose/downloads.yml --env-file .env up -d
Access: http://YOUR_IP:8080
Default login: admin / check logs for the temporary password:
docker logs qbittorrent 2>&1 | grep "temporary password"
7.2 — Configure qBittorrent Settings
These settings are important for disk management and integration with the *Arr apps:
- Settings -> Downloads -> Default Save Path:
/downloads/ - Settings -> Downloads -> Keep incomplete in:
/downloads/incomplete/ - Settings -> BitTorrent -> Seeding limits: Set max ratio to 1.0 (see Section 14 for details)
- Settings -> Web UI: Change the default password
7.3 — SABnzbd (Usenet Downloads — Optional)
Skip this if you only use torrents. Add later if needed.
docker run -d \
--name sabnzbd \
--restart=always \
-p 8888:8080 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/sabnzbd:/config \
-v /mnt/media/downloads/usenet:/downloads \
linuxserver/sabnzbd:latest
Access: http://YOUR_IP:8888
8. *Arr Media Automation Suite
The *Arr apps form the brain of your media automation pipeline. Each one manages a specific media type: movies, TV shows, music, and subtitles. Prowlarr acts as the central indexer manager, and Jellyseerr provides a user-friendly request portal.
8.1 — Prowlarr (Indexer Manager)
Prowlarr manages all your torrent indexers in one place and syncs them to Radarr, Sonarr, and Lidarr automatically.
docker run -d \
--name prowlarr \
--restart=always \
-p 9696:9696 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/prowlarr:/config \
linuxserver/prowlarr:latest
Access: http://YOUR_IP:9696
Setup:
- Settings -> General -> Set authentication (Forms, username/password)
- Indexers -> Add your torrent indexers (1337x, RARBG, etc.)
- Settings -> Apps -> Add Radarr, Sonarr, and Lidarr (after deploying them below)
8.2 — Radarr (Movies)
Radarr automates movie downloads. It searches indexers, sends torrents to qBittorrent, imports completed downloads, renames files, and manages your movie library.
docker run -d \
--name radarr \
--restart=always \
-p 7878:7878 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/radarr:/config \
-v /mnt/media:/media \
linuxserver/radarr:latest
Access: http://YOUR_IP:7878
Setup:
- Settings -> General -> Set authentication
- Settings -> Media Management:
- Root Folder:
/media/movies - Enable "Rename Movies"
- Enable "Use Hardlinks instead of Copy" -> YES (saves disk space when source and destination are on the same filesystem)
- Root Folder:
- Settings -> Download Clients -> Add qBittorrent:
- Host:
YOUR_IP - Port:
8080 - Category:
radarr
- Host:
- Settings -> Quality Profiles -> Select "HD-1080p" as default
- Settings -> Indexers -> (these sync automatically from Prowlarr)
8.3 — Sonarr (TV Shows)
Sonarr is the same concept as Radarr but for TV shows. It monitors series, downloads new episodes automatically, and organizes them by season.
docker run -d \
--name sonarr \
--restart=always \
-p 8989:8989 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/sonarr:/config \
-v /mnt/media:/media \
linuxserver/sonarr:latest
Access: http://YOUR_IP:8989
Setup (same pattern as Radarr):
- Authentication in Settings -> General
- Root Folder:
/media/tv - Download Client: qBittorrent (category:
sonarr) - Quality Profile: HD-1080p
- Enable hardlinks
8.4 — Lidarr (Music)
Lidarr manages your music library the same way Radarr manages movies.
docker run -d \
--name lidarr \
--restart=always \
-p 8686:8686 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/lidarr:/config \
-v /mnt/media:/media \
linuxserver/lidarr:latest
Access: http://YOUR_IP:8686
Root Folder: /media/music, download category: lidarr
8.5 — Bazarr (Automatic Subtitles)
Bazarr automatically downloads subtitles for your movies and TV shows by connecting to Radarr and Sonarr.
docker run -d \
--name bazarr \
--restart=always \
-p 6767:6767 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-v /opt/appdata/bazarr:/config \
-v /mnt/media:/media \
linuxserver/bazarr:latest
Access: http://YOUR_IP:6767
Setup:
- Settings -> Subtitles -> Languages: Add your preferred languages
- Settings -> Providers -> Add OpenSubtitles.com (free account required)
- Settings -> Sonarr/Radarr -> Connect to both using their API keys (found in each app under Settings -> General)
8.6 — Jellyseerr (Request Portal)
Jellyseerr gives your users (family, friends) a Netflix-like interface to browse and request movies and TV shows. Requests are automatically forwarded to Radarr/Sonarr for download.
docker run -d \
--name jellyseerr \
--restart=always \
-p 5055:5055 \
-e TZ=Europe/London \
-v /opt/appdata/jellyseerr:/app/config \
fallenbagel/jellyseerr:latest
Access: http://YOUR_IP:5055
Connect to Jellyfin + Radarr + Sonarr during the setup wizard.
8.7 — Connect Prowlarr to All *Arr Apps
Once all *Arr apps are running, go back to Prowlarr (http://YOUR_IP:9696):
- Settings -> Apps -> Add:
- Radarr: URL
http://YOUR_IP:7878, API key from Radarr -> Settings -> General - Sonarr: URL
http://YOUR_IP:8989, API key from Sonarr -> Settings -> General - Lidarr: URL
http://YOUR_IP:8686, API key from Lidarr -> Settings -> General
- Radarr: URL
- Click "Sync" — all indexers now appear in every *Arr app automatically
9. Jellyfin Media Server
Jellyfin is the heart of the stack — a free, open-source media server that streams your content to any device (web browser, smart TV, phone, tablet).
9.1 — Deploy Jellyfin with Hardware Transcoding
The key to smooth streaming is passing the Intel GPU device into the container for hardware-accelerated transcoding.
docker run -d \
--name jellyfin \
--restart=always \
-p 8096:8096 \
--device=/dev/dri/renderD128:/dev/dri/renderD128 \
--device=/dev/dri/card1:/dev/dri/card1 \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-e JELLYFIN_PublishedServerUrl=http://YOUR_IP:8096 \
-v /opt/appdata/jellyfin:/config \
-v /mnt/media/movies:/data/movies \
-v /mnt/media/tv:/data/tv \
-v /mnt/media/music:/data/music \
-v /mnt/media/books:/data/books \
jellyfin/jellyfin:latest
Note: The GPU device path may be
/dev/dri/card0instead of/dev/dri/card1on your system. Check withls -la /dev/dri/to confirm.
Access: http://YOUR_IP:8096
First-time setup wizard:
- Create an admin account
- Add media libraries:
- Movies ->
/data/movies - Shows ->
/data/tv - Music ->
/data/music
- Movies ->
- Set preferred language and metadata language
9.2 — Enable QSV Hardware Transcoding
In Jellyfin's admin dashboard:
- Dashboard -> Playback -> Transcoding
- Hardware acceleration: Intel QuickSync (QSV)
- Enable hardware decoding for: H264, HEVC, VP9, AV1 (Intel Iris Xe supports all)
- Enable Hardware encoding -> YES
- Enable Tone mapping -> YES
- Preferred hardware encoder: QSV
- Save
9.3 — Verify Transcoding Works
- Play a video in the Jellyfin web player
- During playback, check if hardware transcoding is active:
# Should show GPU activity (render/video percentages above 0%)
sudo intel_gpu_top
# (install with: sudo apt install -y intel-gpu-tools)
9.4 — Client Apps
- Samsung Tizen TV: Install Jellyfin from the Samsung app store
- iPhone/iPad: Install "Jellyfin" from the App Store (free)
- Android: Install "Jellyfin" from the Play Store (free)
- Alternative: Infuse Pro (iOS/tvOS, paid — prettier UI, auto-discovers Jellyfin)
Point all apps at: http://YOUR_IP:8096
For remote access (outside home): Use the Tailscale IP http://100.x.x.x:8096
10. Nextcloud — Self-Hosted Cloud Storage
Nextcloud replaces Google Drive, Dropbox, and iCloud with a self-hosted solution. You can sync files, auto-upload photos from your phone, and share files with links.
Option A: Nextcloud Snap (Simpler)
The Ubuntu snap is the easiest way to run Nextcloud. It includes its own database and web server.
sudo snap install nextcloud
If you need port 80 for Nginx Proxy Manager, remap Nextcloud's HTTP port:
sudo snap set nextcloud ports.http=8088
sudo snap restart nextcloud
Access: http://YOUR_IP:8088
Option B: Nextcloud Docker (More Flexible)
If you prefer Docker for consistency with the rest of the stack:
Step 1 — MariaDB (database):
docker run -d \
--name nextcloud-db \
--restart=always \
-e MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PW \
-e MYSQL_DATABASE=nextcloud \
-e MYSQL_USER=nextcloud \
-e MYSQL_PASSWORD=CHANGE_ME_NC_PW \
-v /opt/appdata/nextcloud-db:/var/lib/mysql \
mariadb:latest \
--transaction-isolation=READ-COMMITTED \
--log-bin=binlog \
--binlog-format=ROW
Step 2 — Nextcloud:
docker run -d \
--name nextcloud \
--restart=always \
-p 8443:80 \
-e MYSQL_HOST=nextcloud-db \
-e MYSQL_DATABASE=nextcloud \
-e MYSQL_USER=nextcloud \
-e MYSQL_PASSWORD=CHANGE_ME_NC_PW \
-e NEXTCLOUD_ADMIN_USER=admin \
-e NEXTCLOUD_ADMIN_PASSWORD=CHANGE_ME_ADMIN_PW \
-e NEXTCLOUD_TRUSTED_DOMAINS="YOUR_IP localhost" \
--link nextcloud-db:nextcloud-db \
-v /opt/appdata/nextcloud:/var/www/html \
-v /mnt/media/nextcloud:/var/www/html/data \
nextcloud:latest
Access: http://YOUR_IP:8443
Phone Setup
- Install the Nextcloud app from your phone's app store
- Server:
http://YOUR_IP:8088(snap) orhttp://YOUR_IP:8443(Docker) — use the Tailscale IP for remote access - Login with admin credentials
- For photo auto-upload: Nextcloud app -> Auto Upload -> Enable
Tip: PhotoSync app (iOS, one-time purchase) provides more reliable photo backup to Nextcloud via WebDAV than the native Nextcloud auto-upload.
11. Media Processing — Tdarr & Ollama
11.1 — Tdarr (Automated Library Transcoding)
Tdarr scans your existing media library and automatically re-encodes files to more efficient codecs (e.g., H.264 -> H.265/HEVC) using Intel QSV hardware acceleration. This can save 40-60% disk space with no visible quality loss.
docker run -d \
--name tdarr \
--restart=always \
-p 8265:8265 \
-p 8266:8266 \
--device=/dev/dri:/dev/dri \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-e serverIP=0.0.0.0 \
-e serverPort=8266 \
-e webUIPort=8265 \
-e internalNode=true \
-e inContainer=true \
-e nodeName=MediaServer \
-v /opt/appdata/tdarr/server:/app/server \
-v /opt/appdata/tdarr/configs:/app/configs \
-v /opt/appdata/tdarr/logs:/app/logs \
-v /mnt/media:/media \
-v /opt/appdata/tdarr/transcode_cache:/temp \
ghcr.io/haveagitgat/tdarr:latest
Access: http://YOUR_IP:8265
Setup:
- Libraries -> Add:
- Movies: Source
/media/movies, Transcode cache/temp - TV: Source
/media/tv, Transcode cache/temp
- Movies: Source
- Plugins -> Use community plugin:
Tdarr_Plugin_MC93_Migz1FFMPEG- Target codec: HEVC (H.265)
- Use hardware: Intel QSV
- Schedule (recommended to prevent daytime CPU load):
- Node -> Schedule -> Enable, set hours like 01:00-07:00
11.2 — Ollama (Local LLM — Optional)
Ollama runs large language models locally on your server. Useful for a self-hosted AI chatbot, or just experimenting with local LLMs.
docker run -d \
--name ollama \
--restart=unless-stopped \
-p 11434:11434 \
-v /opt/appdata/ollama:/root/.ollama \
ollama/ollama:latest
Pull a model:
docker exec ollama ollama pull mistral:7b
# Or a smaller model if RAM is tight:
docker exec ollama ollama pull phi3:mini
Test:
docker exec ollama ollama run mistral:7b "What is a homelab?"
12. Monitoring — Uptime Kuma
Uptime Kuma provides a clean, self-hosted status page that monitors all your services and alerts you when something goes down.
docker run -d \
--name uptime-kuma \
--restart=always \
-p 3001:3001 \
-v /opt/appdata/uptime-kuma:/app/data \
louislam/uptime-kuma:latest
Access: http://YOUR_IP:3001
Add monitors for each service:
| Service | Type | URL |
|---|---|---|
| Jellyfin | HTTP | http://YOUR_IP:8096 |
| Radarr | HTTP | http://YOUR_IP:7878 |
| Sonarr | HTTP | http://YOUR_IP:8989 |
| qBittorrent | HTTP | http://YOUR_IP:8080 |
| Prowlarr | HTTP | http://YOUR_IP:9696 |
| Bazarr | HTTP | http://YOUR_IP:6767 |
| Jellyseerr | HTTP | http://YOUR_IP:5055 |
| AdGuard | HTTP | http://YOUR_IP:8053 |
| Nextcloud | HTTP | http://YOUR_IP:8088 |
Set check interval to 60 seconds. Enable notifications (email, Telegram, Discord, etc.) to be alerted when a service goes down.
13. Integration & Automation — Connecting Everything Together
This is where the magic happens. Once all services are connected, a single movie request triggers a fully automated pipeline.
13.1 — *Arr Download Client Configuration
In Radarr (and repeat for Sonarr, Lidarr):
- Settings -> Download Clients -> Add -> qBittorrent
- Host:
YOUR_IP - Port:
8080 - Username/Password: your qBittorrent credentials
- Category:
radarr(orsonarr,lidarrrespectively) - Test -> Save
- Host:
13.2 — Bazarr Integration
In Bazarr:
- Settings -> Sonarr -> Enable, enter URL + API key
- Settings -> Radarr -> Enable, enter URL + API key
- Settings -> Languages -> Add preferred subtitle languages
- Settings -> Providers -> Add OpenSubtitles.com
13.3 — Jellyseerr Integration
In Jellyseerr (first-time wizard):
- Sign in with Jellyfin -> enter URL + credentials
- Add Radarr server: URL + API key + quality profile + root folder
- Add Sonarr server: URL + API key + quality profile + root folder
- Now users can request movies/TV via
http://YOUR_IP:5055
13.4 — The Complete Request Flow
User requests movie Radarr searches indexers qBittorrent downloads
via Jellyseerr (:5055) --> via Prowlarr (:9696) --> to NVMe: /downloads/torrents/
|
v
Download completes
|
v
Radarr detects completion, qBittorrent seeds to
imports (copies) movie <-- ratio 1.0, then auto-removes
to SSD: /media/movies/ torrent from list
|
v
Radarr deletes source files
from /downloads/torrents/
(removeCompletedDownloads: true)
|
v
Jellyfin detects new movie Bazarr auto-downloads
in /data/movies/ library --> subtitles (.srt/.ass)
|
v
Tdarr (optional) transcodes
to H.265 using Intel QSV
|
v
Movie available for playback
on all devices (TV, phone, web)
13.5 — End-to-End Test
- Go to Jellyseerr -> Search for a movie -> Request it
- Check Radarr -> should show the movie as "Searching"
- Check qBittorrent -> should start downloading
- Wait for download to complete
- Check Jellyfin -> movie should appear in library
- Play the movie -> verify hardware transcoding works
- Check Bazarr -> subtitles should auto-download
If all steps work, your media automation pipeline is fully operational.
13.6 — Where Files Live (Filesystem Map)
NVMe (boot drive) -- Fast, temporary storage Second Drive -- Movie library
+----------------------------------+ +------------------------------+
| /mnt/media/downloads/torrents/ | | /mnt/ssd/movies/ |
| +-- Movie.Name.1080p.x264/ |--(Radarr)----| +-- Inception (2010)/ |
| | +-- movie.mkv (temp) | copies & | | +-- Inception.mkv |
| +-- (auto-cleaned after 7d) | deletes | +-- Interstellar (2014)/ |
| | source | | +-- Interstellar.mkv |
| /mnt/media/tv/ | | +-- ... |
| +-- Show Name/Season 01/ | | |
| +-- ... | | Bind-mounted to: |
| | | /mnt/media/movies/ |
| /mnt/media/music/ | | (transparent to containers) |
| /mnt/media/books/ | +------------------------------+
+----------------------------------+
Note: When source (NVMe) and destination (SSD) are on different filesystems, Radarr uses copy instead of hardlinks. This is expected and correct.
14. Backup & Disk Protection
14.1 — Config Backup Script
Your container configurations in /opt/appdata are the most important thing to back up. Media can be re-downloaded, but configs contain your settings, databases, and user accounts.
Create /opt/docker/backup.sh:
#!/bin/bash
# Backup all container configs
BACKUP_DIR="/mnt/media/backups"
DATE=$(date +%Y%m%d)
mkdir -p $BACKUP_DIR
# Stop containers that use databases for clean backup
docker stop nextcloud-db
# Create tarball of all configs
tar -czf $BACKUP_DIR/appdata-$DATE.tar.gz -C /opt appdata
# Restart stopped containers
docker start nextcloud-db
# Keep only last 7 backups
ls -tp $BACKUP_DIR/appdata-*.tar.gz | tail -n +8 | xargs -I {} rm -- {}
echo "Backup completed: $BACKUP_DIR/appdata-$DATE.tar.gz"
chmod +x /opt/docker/backup.sh
# Test it
/opt/docker/backup.sh
# Schedule weekly (Sunday 3AM)
crontab -e
# Add this line:
# 0 3 * * 0 /opt/docker/backup.sh >> /var/log/backup.log 2>&1
14.2 — Disk Protection — Three Layers
Running a media server on limited storage (especially a single NVMe) can quickly fill the disk if downloads are not cleaned up. Here is a three-layer protection strategy:
| Layer | Component | Action | Trigger |
|---|---|---|---|
| 1 | qBittorrent | Auto-remove torrent after seeding | Ratio 1.0 / 24h max / 24h inactive |
| 2 | Radarr/Sonarr | Delete source files after import | removeCompletedDownloads: true |
| 3 | disk-guard.sh | Clean old downloads + Docker/snap/apt | Every 15 min via cron |
Layer 1: qBittorrent Seeding Limits
Add to /opt/appdata/qbittorrent/qBittorrent/qBittorrent.conf under [BitTorrent]:
Session\GlobalMaxRatio=1
Session\GlobalMaxSeedingMinutes=1440
Session\GlobalMaxInactiveSeedingMinutes=1440
Session\ShareLimitAction=Remove
- GlobalMaxRatio=1 — Stop seeding after uploading 1x the file size
- GlobalMaxSeedingMinutes=1440 — Max 24 hours of seeding regardless of ratio
- GlobalMaxInactiveSeedingMinutes=1440 — Remove if no upload activity for 24 hours
- ShareLimitAction=Remove — Remove torrent from list when limits are reached
Layer 2: *Arr Completed Download Handling
Already configured in Radarr and Sonarr:
removeCompletedDownloads: true— After import, remove torrent + delete source filesautoRedownloadFailed: true— Retry if download fails
Layer 3: disk-guard.sh — Automated Disk Cleanup
Create /opt/docker/disk-guard.sh and schedule it via root crontab:
# Root crontab entry:
*/15 * * * * /opt/docker/disk-guard.sh
| Disk Usage | Action |
|---|---|
| Always | Clean Docker unused images, journal logs > 7 days |
| >= 85% | Delete torrent downloads older than 7 days, aggressive Docker prune |
| >= 95% | Emergency: Delete ALL torrent downloads, clear snap + apt cache |
To check its activity:
# View recent log entries
tail -20 /var/log/disk-guard.log
# Manual run
sudo /opt/docker/disk-guard.sh
# Check current disk usage
df -h / /mnt/ssd
14.3 — SSD Expansion (Adding a Second Drive)
When you add a second drive (SSD or HDD) for extra media storage:
# Identify the new drive
lsblk
# Create GPT partition table + single partition
sudo fdisk /dev/sda
# g (new GPT), n (new partition), 1, Enter, Enter, w (write)
# Format as ext4
sudo mkfs.ext4 -L media-ssd /dev/sda1
# Create mount point and mount
sudo mkdir -p /mnt/ssd
sudo mount /dev/sda1 /mnt/ssd
# Get UUID and add to fstab for persistent mounting
UUID=$(sudo blkid -s UUID -o value /dev/sda1)
echo "UUID=$UUID /mnt/ssd ext4 defaults 0 2" | sudo tee -a /etc/fstab
Migrate content and create a bind mount:
# Create movies directory on the new drive
sudo mkdir -p /mnt/ssd/movies
sudo chown YOUR_USER:YOUR_USER /mnt/ssd /mnt/ssd/movies
# Copy movies to the new drive
cp -av /mnt/media/movies/* /mnt/ssd/movies/
# Verify both locations match
ls /mnt/media/movies/ | wc -l
ls /mnt/ssd/movies/ | wc -l
# Remove originals from the boot drive
rm -rf /mnt/media/movies/*
# Bind mount (immediate)
sudo mount --bind /mnt/ssd/movies /mnt/media/movies
# Persist in fstab
echo "/mnt/ssd/movies /mnt/media/movies none bind 0 0" | sudo tee -a /etc/fstab
# Restart Jellyfin to pick up the bind mount
docker restart jellyfin
Now /mnt/media/movies transparently serves content from the second drive. All containers work without any Docker changes.
14.4 — Reduce ext4 Reserved Blocks
By default, ext4 reserves 5% of disk space for root. On a homelab server, 1% is sufficient:
sudo tune2fs -m 1 /dev/mapper/ubuntu--vg-ubuntu--lv
This recovers ~16GB of usable space on a 466GB drive.
14.5 — DNS Fix (Tailscale + Disk Full Edge Case)
If the disk fills up, Tailscale's MagicDNS can break and hijack /etc/resolv.conf, causing DNS resolution to fail. Fix:
sudo tailscale set --accept-dns=false
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved
15. Dashboard — Landing Page with Service Links
A simple landing page gives you one-click access to all your services. You can build a custom HTML page or use an existing dashboard tool like Homer, Homarr, or Dashy.
The dashboard should include:
- Links to all services with their ports
- System status overview
- Quick-access to the most used services (Jellyfin, Jellyseerr, qBittorrent)
- Optionally, an AI chat widget connected to your local Ollama instance
Deploy a simple dashboard container on port 8888 (or whichever port you prefer) and configure it with links to all your services from the port map in Section 1.
16. Troubleshooting — Common Issues and Fixes
Container Won't Start
docker logs <container_name>
# Check for permission errors — usually fix with:
sudo chown -R 1000:1000 /opt/appdata/<container_name>
Hardware Transcoding Not Working
# Check GPU device exists
ls -la /dev/dri/
# Should show: card0 (or card1) and renderD128
# Check VA-API driver
vainfo
# Should show "iHD driver"
# Fix permissions
sudo usermod -aG render $USER
sudo usermod -aG video $USER
# Log out/in, then restart Jellyfin container
docker restart jellyfin
Laptop Overheating
# Install thermal management
sudo apt install -y thermald
# Check temperatures
sensors
# (install with: sudo apt install -y lm-sensors && sudo sensors-detect)
Cannot Access Services Remotely
# Verify Tailscale is connected
tailscale status
# Check if firewall is blocking
sudo ufw status
# If active, allow ports:
sudo ufw allow 8096/tcp # Jellyfin
sudo ufw allow 5055/tcp # Jellyseerr
# (add other ports as needed)
USB Drive Disconnects Randomly
# Disable USB autosuspend
echo -1 | sudo tee /sys/module/usbcore/parameters/autosuspend
# Make permanent — add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub:
# usbcore.autosuspend=-1
sudo update-grub
sudo reboot
Disk Full / No Internet / Services Broken
# Check disk usage
df -h / /mnt/ssd
# Run disk cleanup manually
sudo /opt/docker/disk-guard.sh
# Check what is eating space
du -sh /mnt/media/downloads/torrents/ # Torrent downloads
du -sh /var/lib/docker/ # Docker images/layers
du -sh /var/lib/snapd/cache/ # Snap cache
# If DNS is broken (Tailscale took over resolv.conf):
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved
# Check disk-guard log
tail -20 /var/log/disk-guard.log
Quick Reference: Container Management
# See all running containers
docker ps
# See all containers (including stopped)
docker ps -a
# View container logs
docker logs <container_name>
docker logs -f <container_name> # Follow (live)
# Restart a container
docker restart <container_name>
# Stop / Start
docker stop <container_name>
docker start <container_name>
# Remove a container (data in /opt/appdata is preserved)
docker stop <container_name> && docker rm <container_name>
# Update a container manually
docker pull <image_name>:latest
docker stop <container_name> && docker rm <container_name>
# Then re-run the original docker run command
# Check Docker disk usage
docker system df
# Clean up unused images/containers
docker system prune -a
Quick Reference: Useful Paths
| Path | Contents | Disk |
|---|---|---|
/opt/appdata/ |
All container configs — BACK THIS UP | Boot drive |
/opt/docker/ |
Docker compose files, .env, scripts | Boot drive |
/mnt/media/ |
All media content | Boot drive |
/mnt/media/downloads/ |
Active torrent downloads (auto-cleaned) | Boot drive |
/mnt/media/movies/ |
Radarr-managed movies (bind mount to second drive) | Second drive |
/mnt/media/tv/ |
Sonarr-managed TV shows | Boot drive |
/mnt/media/music/ |
Lidarr-managed music | Boot drive |
/mnt/media/books/ |
Books | Boot drive |
/mnt/media/nextcloud/ |
Nextcloud user data | Boot drive |
/mnt/ssd/ |
Second drive mount point | Second drive |
/opt/docker/disk-guard.sh |
Disk protection script (cron every 15 min) | Boot drive |
/var/log/disk-guard.log |
Disk guard log | Boot drive |
17. Conclusion
You now have a fully automated, self-hosted media server running on a repurposed laptop. Here is what the complete stack provides:
- Jellyfin streams your movies, TV shows, and music to any device with Intel QSV hardware transcoding
- *The Arr suite (Radarr, Sonarr, Lidarr, Prowlarr) automates finding, downloading, and organizing media
- Jellyseerr gives users a Netflix-like request interface
- Bazarr automatically downloads subtitles in your preferred languages
- qBittorrent handles downloads with automatic seeding limits and cleanup
- Tdarr re-encodes your library to H.265 for 40-60% space savings
- AdGuard Home blocks ads network-wide at the DNS level
- Nginx Proxy Manager provides clean URLs with SSL certificates
- Tailscale enables secure remote access from anywhere
- Nextcloud replaces cloud storage services
- Uptime Kuma monitors everything and alerts you when services go down
- Watchtower keeps all containers automatically updated
- Portainer gives you a GUI for Docker management
- Three-layer disk protection prevents the disk from filling up
Next Steps
- Add torrent indexers in Prowlarr (
:9696) for better search results - Add subtitle providers and languages in Bazarr (
:6767) - Set up Uptime Kuma monitors for each service (
:3001) - Configure NPM proxy hosts for clean domain-based URLs (
:81) - Set up Nextcloud auto-upload on your phone for photo backup
- Consider adding a VPN (Gluetun) in front of qBittorrent for privacy
- Explore Navidrome (
:4533) if you want a dedicated music streaming server with Subsonic-compatible mobile apps
The entire stack runs on ~15-20W at idle, costs nothing per month, and gives you complete ownership of your media library. Every service is containerized, backed up weekly, and auto-updated -- set it and forget it.