Tools: Homelab Media Server — Complete Docker Stack on Repurposed Laptop (2026)

Tools: Homelab Media Server — Complete Docker Stack on Repurposed Laptop (2026)

Tutorial 44: Homelab Media Server — Complete Docker Stack on Repurposed Laptop

Table of Contents

1. Introduction — Why Self-Host a Media Server

The Case for Self-Hosting

Why a Laptop?

What We Are Building

Service Overview — Port Map

2. Hardware Overview & Storage Strategy

Recommended Hardware Specs

Laptop-Specific Considerations

Storage Strategy — Dual-Drive Setup

3. Ubuntu Installation & System Configuration

3.1 — Install Ubuntu 24.04 LTS

3.2 — Update System

3.3 — Disable Lid Close Sleep

3.4 — Set Static IP (Recommended)

3.5 — Install Intel Quick Sync (QSV) Drivers

3.6 — Install Docker & Docker Compose

3.7 — Create Directory Structure

3.8 — Configure Power Management for 24/7 Operation

3.9 — Set Up Automatic Security Updates

4. Environment Configuration — Centralized .env File

5. Management Stack — Portainer, Tailscale, Watchtower

5.1 — Portainer (Docker GUI)

5.2 — Tailscale (Remote Access VPN)

5.3 — Watchtower (Automatic Container Updates)

6. Network & Security — AdGuard Home & Nginx Proxy Manager

6.1 — AdGuard Home (Network-Wide Ad Blocking)

6.2 — Nginx Proxy Manager (Reverse Proxy + SSL)

7. Download Stack — qBittorrent

7.1 — Deploy qBittorrent

7.2 — Configure qBittorrent Settings

7.3 — SABnzbd (Usenet Downloads — Optional)

8. *Arr Media Automation Suite

8.1 — Prowlarr (Indexer Manager)

8.2 — Radarr (Movies)

8.3 — Sonarr (TV Shows)

8.4 — Lidarr (Music)

8.5 — Bazarr (Automatic Subtitles)

8.6 — Jellyseerr (Request Portal)

8.7 — Connect Prowlarr to All *Arr Apps

9. Jellyfin Media Server

9.1 — Deploy Jellyfin with Hardware Transcoding

9.2 — Enable QSV Hardware Transcoding

9.3 — Verify Transcoding Works

9.4 — Client Apps

10. Nextcloud — Self-Hosted Cloud Storage

Option A: Nextcloud Snap (Simpler)

Option B: Nextcloud Docker (More Flexible)

Phone Setup

11. Media Processing — Tdarr & Ollama

11.1 — Tdarr (Automated Library Transcoding)

11.2 — Ollama (Local LLM — Optional)

12. Monitoring — Uptime Kuma

13. Integration & Automation — Connecting Everything Together

13.1 — *Arr Download Client Configuration

13.2 — Bazarr Integration

13.3 — Jellyseerr Integration

13.4 — The Complete Request Flow

13.5 — End-to-End Test

13.6 — Where Files Live (Filesystem Map)

14. Backup & Disk Protection

14.1 — Config Backup Script

14.2 — Disk Protection — Three Layers

Layer 1: qBittorrent Seeding Limits

Layer 2: *Arr Completed Download Handling

Layer 3: disk-guard.sh — Automated Disk Cleanup

14.3 — SSD Expansion (Adding a Second Drive)

14.4 — Reduce ext4 Reserved Blocks

14.5 — DNS Fix (Tailscale + Disk Full Edge Case)

15. Dashboard — Landing Page with Service Links

16. Troubleshooting — Common Issues and Fixes

Container Won't Start

Hardware Transcoding Not Working

Laptop Overheating

Cannot Access Services Remotely

USB Drive Disconnects Randomly

Disk Full / No Internet / Services Broken

Quick Reference: Container Management

Quick Reference: Useful Paths

17. Conclusion

Next Steps 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 minutesPrerequisites: A laptop or mini PC with an Intel CPU (6th gen+ for QSV), 16GB+ RAM, Ubuntu 24.04 installedTechnologies: Docker, Jellyfin, Radarr, Sonarr, Prowlarr, qBittorrent, AdGuard, Nginx Proxy Manager, Tailscale, Tdarr, Nextcloud, PortainerOS: Ubuntu 24.04 LTS Streaming services keep raising prices, splitting content across platforms, and removing shows without warning. A self-hosted media server gives you: Repurposing an old laptop is one of the best homelab decisions you can make: 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. By the end of this tutorial, you will have a fully automated media server with this architecture: 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. 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. 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. Edit /etc/systemd/logind.conf and set: Then restart the login manager: If your system uses NetworkManager (Ubuntu Desktop), use nmcli: Verify the installation: You should see output like: 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-driver package includes all commonly needed codecs. If you need extra codec profiles later, install intel-media-va-driver-non-free instead. If your boot drive uses LVM (common with Ubuntu Server), expand it to use the full disk first: Create the media directories: For laptops, also install thermal management: 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: 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. Portainer gives you a web-based GUI for managing all your Docker containers, images, volumes, and networks. Access: http://YOUR_IP:9000 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. 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): Watchtower monitors your running containers and automatically pulls updated images, recreating containers with the same configuration. 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 set DOCKER_API_VERSION accordingly. 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. First-time setup at http://YOUR_IP:3000: 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. 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: Access: http://YOUR_IP:81Default 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). 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. Create the compose file at /opt/docker/compose/downloads.yml: Access: http://YOUR_IP:8080Default login: admin / check logs for the temporary password: These settings are important for disk management and integration with the *Arr apps: Skip this if you only use torrents. Add later if needed. Access: http://YOUR_IP:8888 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. Prowlarr manages all your torrent indexers in one place and syncs them to Radarr, Sonarr, and Lidarr automatically. Access: http://YOUR_IP:9696 Radarr automates movie downloads. It searches indexers, sends torrents to qBittorrent, imports completed downloads, renames files, and manages your movie library. Access: http://YOUR_IP:7878 Sonarr is the same concept as Radarr but for TV shows. It monitors series, downloads new episodes automatically, and organizes them by season. Access: http://YOUR_IP:8989 Setup (same pattern as Radarr): Lidarr manages your music library the same way Radarr manages movies. Access: http://YOUR_IP:8686Root Folder: /media/music, download category: lidarr Bazarr automatically downloads subtitles for your movies and TV shows by connecting to Radarr and Sonarr. Access: http://YOUR_IP:6767 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. Access: http://YOUR_IP:5055Connect to Jellyfin + Radarr + Sonarr during the setup wizard. Once all *Arr apps are running, go back to Prowlarr (http://YOUR_IP:9696): 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). The key to smooth streaming is passing the Intel GPU device into the container for hardware-accelerated transcoding. Note: The GPU device path may be /dev/dri/card0 instead of /dev/dri/card1 on your system. Check with ls -la /dev/dri/ to confirm. Access: http://YOUR_IP:8096 First-time setup wizard: In Jellyfin's admin dashboard: Point all apps at: http://YOUR_IP:8096

For remote access (outside home): Use the Tailscale IP http://100.x.x.x:8096 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. The Ubuntu snap is the easiest way to run Nextcloud. It includes its own database and web server. If you need port 80 for Nginx Proxy Manager, remap Nextcloud's HTTP port: Access: http://YOUR_IP:8088 If you prefer Docker for consistency with the rest of the stack: Step 1 — MariaDB (database): Access: http://YOUR_IP:8443 Tip: PhotoSync app (iOS, one-time purchase) provides more reliable photo backup to Nextcloud via WebDAV than the native Nextcloud auto-upload. 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. Access: http://YOUR_IP:8265 Ollama runs large language models locally on your server. Useful for a self-hosted AI chatbot, or just experimenting with local LLMs. Uptime Kuma provides a clean, self-hosted status page that monitors all your services and alerts you when something goes down. Access: http://YOUR_IP:3001 Add monitors for each service: Set check interval to 60 seconds. Enable notifications (email, Telegram, Discord, etc.) to be alerted when a service goes down. This is where the magic happens. Once all services are connected, a single movie request triggers a fully automated pipeline. In Radarr (and repeat for Sonarr, Lidarr): In Jellyseerr (first-time wizard): If all steps work, your media automation pipeline is fully operational. Note: When source (NVMe) and destination (SSD) are on different filesystems, Radarr uses copy instead of hardlinks. This is expected and correct. 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: 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: Add to /opt/appdata/qbittorrent/qBittorrent/qBittorrent.conf under [BitTorrent]: Already configured in Radarr and Sonarr: Create /opt/docker/disk-guard.sh and schedule it via root crontab: To check its activity: When you add a second drive (SSD or HDD) for extra media storage: Migrate content and create a bind mount: Now /mnt/media/movies transparently serves content from the second drive. All containers work without any Docker changes. By default, ext4 reserves 5% of disk space for root. On a homelab server, 1% is sufficient: This recovers ~16GB of usable space on a 466GB drive. If the disk fills up, Tailscale's MagicDNS can break and hijack /etc/resolv.conf, causing DNS resolution to fail. Fix: 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: 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. You now have a fully automated, self-hosted media server running on a repurposed laptop. Here is what the complete stack provides: 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ 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) 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) 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) NVMe SSD (boot drive) — OS, Docker, downloads, configs ├── / OS + Docker (~20GB) ├── /opt/appdata Container configs + databases (~5-10GB) ├── /opt/-weight: 500;">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/-weight: 500;">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 NVMe SSD (boot drive) — OS, Docker, downloads, configs ├── / OS + Docker (~20GB) ├── /opt/appdata Container configs + databases (~5-10GB) ├── /opt/-weight: 500;">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/-weight: 500;">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 NVMe SSD (boot drive) — OS, Docker, downloads, configs ├── / OS + Docker (~20GB) ├── /opt/appdata Container configs + databases (~5-10GB) ├── /opt/-weight: 500;">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/-weight: 500;">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 -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo reboot -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo reboot -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update && -weight: 600;">sudo -weight: 500;">apt -weight: 500;">upgrade -y -weight: 600;">sudo reboot HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore HandleLidSwitch=ignore HandleLidSwitchExternalPower=ignore HandleLidSwitchDocked=ignore -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-logind -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-logind -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-logind # For Ethernet: -weight: 600;">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" -weight: 600;">sudo nmcli con up static-eth # For WiFi (adjust connection name from 'nmcli con show'): -weight: 600;">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 # For Ethernet: -weight: 600;">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" -weight: 600;">sudo nmcli con up static-eth # For WiFi (adjust connection name from 'nmcli con show'): -weight: 600;">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 # For Ethernet: -weight: 600;">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" -weight: 600;">sudo nmcli con up static-eth # For WiFi (adjust connection name from 'nmcli con show'): -weight: 600;">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 -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-media-va-driver vainfo mesa-va-drivers -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-media-va-driver vainfo mesa-va-drivers -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-media-va-driver vainfo mesa-va-drivers VA-API version: 1.20 Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0 VA-API version: 1.20 Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0 VA-API version: 1.20 Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0 # Install Docker using the official convenience script -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # Add your user to the -weight: 500;">docker group (avoids needing -weight: 600;">sudo for -weight: 500;">docker commands) -weight: 600;">sudo usermod -aG -weight: 500;">docker $USER # Log out and back in for group change to take effect # Verify: -weight: 500;">docker --version -weight: 500;">docker compose version # Install Docker using the official convenience script -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # Add your user to the -weight: 500;">docker group (avoids needing -weight: 600;">sudo for -weight: 500;">docker commands) -weight: 600;">sudo usermod -aG -weight: 500;">docker $USER # Log out and back in for group change to take effect # Verify: -weight: 500;">docker --version -weight: 500;">docker compose version # Install Docker using the official convenience script -weight: 500;">curl -fsSL https://get.-weight: 500;">docker.com | sh # Add your user to the -weight: 500;">docker group (avoids needing -weight: 600;">sudo for -weight: 500;">docker commands) -weight: 600;">sudo usermod -aG -weight: 500;">docker $USER # Log out and back in for group change to take effect # Verify: -weight: 500;">docker --version -weight: 500;">docker compose version # Expand LVM to use full disk (Ubuntu often allocates only ~100GB during -weight: 500;">install) -weight: 600;">sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv -weight: 600;">sudo resize2fs /dev/ubuntu-vg/ubuntu-lv # Expand LVM to use full disk (Ubuntu often allocates only ~100GB during -weight: 500;">install) -weight: 600;">sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv -weight: 600;">sudo resize2fs /dev/ubuntu-vg/ubuntu-lv # Expand LVM to use full disk (Ubuntu often allocates only ~100GB during -weight: 500;">install) -weight: 600;">sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv -weight: 600;">sudo resize2fs /dev/ubuntu-vg/ubuntu-lv # Create all directories -weight: 600;">sudo mkdir -p /opt/appdata /opt/-weight: 500;">docker/compose -weight: 600;">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) -weight: 600;">sudo chown -R 1000:1000 /opt/appdata /opt/-weight: 500;">docker /mnt/media # Create all directories -weight: 600;">sudo mkdir -p /opt/appdata /opt/-weight: 500;">docker/compose -weight: 600;">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) -weight: 600;">sudo chown -R 1000:1000 /opt/appdata /opt/-weight: 500;">docker /mnt/media # Create all directories -weight: 600;">sudo mkdir -p /opt/appdata /opt/-weight: 500;">docker/compose -weight: 600;">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) -weight: 600;">sudo chown -R 1000:1000 /opt/appdata /opt/-weight: 500;">docker /mnt/media # Disable sleep/suspend/hibernate -weight: 600;">sudo -weight: 500;">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 # Disable sleep/suspend/hibernate -weight: 600;">sudo -weight: 500;">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 # Disable sleep/suspend/hibernate -weight: 600;">sudo -weight: 500;">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 -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y unattended-upgrades -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y unattended-upgrades -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y unattended-upgrades -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald # 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 # 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 # 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 -weight: 500;">docker run -d \ --name portainer \ ---weight: 500;">restart=always \ -p 9000:9000 \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ -v /opt/appdata/portainer:/data \ portainer/portainer-ce:latest -weight: 500;">docker run -d \ --name portainer \ ---weight: 500;">restart=always \ -p 9000:9000 \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ -v /opt/appdata/portainer:/data \ portainer/portainer-ce:latest -weight: 500;">docker run -d \ --name portainer \ ---weight: 500;">restart=always \ -p 9000:9000 \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ -v /opt/appdata/portainer:/data \ portainer/portainer-ce:latest # Install Tailscale directly on the host (not Docker — more reliable on laptops) -weight: 500;">curl -fsSL https://tailscale.com/-weight: 500;">install.sh | sh # Start and authenticate -weight: 600;">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 -weight: 500;">status # Install Tailscale directly on the host (not Docker — more reliable on laptops) -weight: 500;">curl -fsSL https://tailscale.com/-weight: 500;">install.sh | sh # Start and authenticate -weight: 600;">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 -weight: 500;">status # Install Tailscale directly on the host (not Docker — more reliable on laptops) -weight: 500;">curl -fsSL https://tailscale.com/-weight: 500;">install.sh | sh # Start and authenticate -weight: 600;">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 -weight: 500;">status -weight: 600;">sudo tailscale set --accept-dns=false -weight: 600;">sudo tailscale set --accept-dns=false -weight: 500;">docker run -d \ --name watchtower \ ---weight: 500;">restart=always \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">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 -weight: 500;">docker run -d \ --name watchtower \ ---weight: 500;">restart=always \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">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 -weight: 500;">docker run -d \ --name watchtower \ ---weight: 500;">restart=always \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">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 -weight: 500;">docker run -d \ --name adguardhome \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name adguardhome \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name adguardhome \ ---weight: 500;">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 -weight: 600;">sudo snap set nextcloud ports.http=8088 && -weight: 600;">sudo snap -weight: 500;">restart nextcloud -weight: 600;">sudo snap set nextcloud ports.http=8088 && -weight: 600;">sudo snap -weight: 500;">restart nextcloud -weight: 500;">docker run -d \ --name nginx-proxy-manager \ ---weight: 500;">restart=always \ -p 80:80 \ -p 443:443 \ -p 81:81 \ -v /opt/appdata/-weight: 500;">npm/data:/data \ -v /opt/appdata/-weight: 500;">npm/letsencrypt:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest -weight: 500;">docker run -d \ --name nginx-proxy-manager \ ---weight: 500;">restart=always \ -p 80:80 \ -p 443:443 \ -p 81:81 \ -v /opt/appdata/-weight: 500;">npm/data:/data \ -v /opt/appdata/-weight: 500;">npm/letsencrypt:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest -weight: 500;">docker run -d \ --name nginx-proxy-manager \ ---weight: 500;">restart=always \ -p 80:80 \ -p 443:443 \ -p 81:81 \ -v /opt/appdata/-weight: 500;">npm/data:/data \ -v /opt/appdata/-weight: 500;">npm/letsencrypt:/etc/letsencrypt \ jc21/nginx-proxy-manager:latest 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 -weight: 500;">restart: always 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 -weight: 500;">restart: always 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 -weight: 500;">restart: always cd /opt/-weight: 500;">docker -weight: 500;">docker compose -f compose/downloads.yml --env-file .env up -d cd /opt/-weight: 500;">docker -weight: 500;">docker compose -f compose/downloads.yml --env-file .env up -d cd /opt/-weight: 500;">docker -weight: 500;">docker compose -f compose/downloads.yml --env-file .env up -d -weight: 500;">docker logs qbittorrent 2>&1 | grep "temporary password" -weight: 500;">docker logs qbittorrent 2>&1 | grep "temporary password" -weight: 500;">docker logs qbittorrent 2>&1 | grep "temporary password" -weight: 500;">docker run -d \ --name sabnzbd \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name sabnzbd \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name sabnzbd \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name prowlarr \ ---weight: 500;">restart=always \ -p 9696:9696 \ -e PUID=1000 \ -e PGID=1000 \ -e TZ=Europe/London \ -v /opt/appdata/prowlarr:/config \ linuxserver/prowlarr:latest -weight: 500;">docker run -d \ --name prowlarr \ ---weight: 500;">restart=always \ -p 9696:9696 \ -e PUID=1000 \ -e PGID=1000 \ -e TZ=Europe/London \ -v /opt/appdata/prowlarr:/config \ linuxserver/prowlarr:latest -weight: 500;">docker run -d \ --name prowlarr \ ---weight: 500;">restart=always \ -p 9696:9696 \ -e PUID=1000 \ -e PGID=1000 \ -e TZ=Europe/London \ -v /opt/appdata/prowlarr:/config \ linuxserver/prowlarr:latest -weight: 500;">docker run -d \ --name radarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name radarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name radarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name sonarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name sonarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name sonarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name lidarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name lidarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name lidarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name bazarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name bazarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name bazarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name jellyseerr \ ---weight: 500;">restart=always \ -p 5055:5055 \ -e TZ=Europe/London \ -v /opt/appdata/jellyseerr:/app/config \ fallenbagel/jellyseerr:latest -weight: 500;">docker run -d \ --name jellyseerr \ ---weight: 500;">restart=always \ -p 5055:5055 \ -e TZ=Europe/London \ -v /opt/appdata/jellyseerr:/app/config \ fallenbagel/jellyseerr:latest -weight: 500;">docker run -d \ --name jellyseerr \ ---weight: 500;">restart=always \ -p 5055:5055 \ -e TZ=Europe/London \ -v /opt/appdata/jellyseerr:/app/config \ fallenbagel/jellyseerr:latest -weight: 500;">docker run -d \ --name jellyfin \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name jellyfin \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name jellyfin \ ---weight: 500;">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 # Should show GPU activity (render/video percentages above 0%) -weight: 600;">sudo intel_gpu_top # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-gpu-tools) # Should show GPU activity (render/video percentages above 0%) -weight: 600;">sudo intel_gpu_top # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-gpu-tools) # Should show GPU activity (render/video percentages above 0%) -weight: 600;">sudo intel_gpu_top # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y intel-gpu-tools) -weight: 600;">sudo snap -weight: 500;">install nextcloud -weight: 600;">sudo snap -weight: 500;">install nextcloud -weight: 600;">sudo snap -weight: 500;">install nextcloud -weight: 600;">sudo snap set nextcloud ports.http=8088 -weight: 600;">sudo snap -weight: 500;">restart nextcloud -weight: 600;">sudo snap set nextcloud ports.http=8088 -weight: 600;">sudo snap -weight: 500;">restart nextcloud -weight: 600;">sudo snap set nextcloud ports.http=8088 -weight: 600;">sudo snap -weight: 500;">restart nextcloud -weight: 500;">docker run -d \ --name nextcloud-db \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name nextcloud-db \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name nextcloud-db \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name nextcloud \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name nextcloud \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name nextcloud \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name tdarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name tdarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name tdarr \ ---weight: 500;">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 -weight: 500;">docker run -d \ --name ollama \ ---weight: 500;">restart=unless-stopped \ -p 11434:11434 \ -v /opt/appdata/ollama:/root/.ollama \ ollama/ollama:latest -weight: 500;">docker run -d \ --name ollama \ ---weight: 500;">restart=unless-stopped \ -p 11434:11434 \ -v /opt/appdata/ollama:/root/.ollama \ ollama/ollama:latest -weight: 500;">docker run -d \ --name ollama \ ---weight: 500;">restart=unless-stopped \ -p 11434:11434 \ -v /opt/appdata/ollama:/root/.ollama \ ollama/ollama:latest -weight: 500;">docker exec ollama ollama pull mistral:7b # Or a smaller model if RAM is tight: -weight: 500;">docker exec ollama ollama pull phi3:mini -weight: 500;">docker exec ollama ollama pull mistral:7b # Or a smaller model if RAM is tight: -weight: 500;">docker exec ollama ollama pull phi3:mini -weight: 500;">docker exec ollama ollama pull mistral:7b # Or a smaller model if RAM is tight: -weight: 500;">docker exec ollama ollama pull phi3:mini -weight: 500;">docker exec ollama ollama run mistral:7b "What is a homelab?" -weight: 500;">docker exec ollama ollama run mistral:7b "What is a homelab?" -weight: 500;">docker exec ollama ollama run mistral:7b "What is a homelab?" -weight: 500;">docker run -d \ --name uptime-kuma \ ---weight: 500;">restart=always \ -p 3001:3001 \ -v /opt/appdata/uptime-kuma:/app/data \ louislam/uptime-kuma:latest -weight: 500;">docker run -d \ --name uptime-kuma \ ---weight: 500;">restart=always \ -p 3001:3001 \ -v /opt/appdata/uptime-kuma:/app/data \ louislam/uptime-kuma:latest -weight: 500;">docker run -d \ --name uptime-kuma \ ---weight: 500;">restart=always \ -p 3001:3001 \ -v /opt/appdata/uptime-kuma:/app/data \ louislam/uptime-kuma:latest 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) 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) 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) 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/ | +------------------------------+ +----------------------------------+ 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/ | +------------------------------+ +----------------------------------+ 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/ | +------------------------------+ +----------------------------------+ #!/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 -weight: 500;">docker -weight: 500;">stop nextcloud-db # Create tarball of all configs tar -czf $BACKUP_DIR/appdata-$DATE.tar.gz -C /opt appdata # Restart stopped containers -weight: 500;">docker -weight: 500;">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" #!/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 -weight: 500;">docker -weight: 500;">stop nextcloud-db # Create tarball of all configs tar -czf $BACKUP_DIR/appdata-$DATE.tar.gz -C /opt appdata # Restart stopped containers -weight: 500;">docker -weight: 500;">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" #!/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 -weight: 500;">docker -weight: 500;">stop nextcloud-db # Create tarball of all configs tar -czf $BACKUP_DIR/appdata-$DATE.tar.gz -C /opt appdata # Restart stopped containers -weight: 500;">docker -weight: 500;">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/-weight: 500;">docker/backup.sh # Test it /opt/-weight: 500;">docker/backup.sh # Schedule weekly (Sunday 3AM) crontab -e # Add this line: # 0 3 * * 0 /opt/-weight: 500;">docker/backup.sh >> /var/log/backup.log 2>&1 chmod +x /opt/-weight: 500;">docker/backup.sh # Test it /opt/-weight: 500;">docker/backup.sh # Schedule weekly (Sunday 3AM) crontab -e # Add this line: # 0 3 * * 0 /opt/-weight: 500;">docker/backup.sh >> /var/log/backup.log 2>&1 chmod +x /opt/-weight: 500;">docker/backup.sh # Test it /opt/-weight: 500;">docker/backup.sh # Schedule weekly (Sunday 3AM) crontab -e # Add this line: # 0 3 * * 0 /opt/-weight: 500;">docker/backup.sh >> /var/log/backup.log 2>&1 Session\GlobalMaxRatio=1 Session\GlobalMaxSeedingMinutes=1440 Session\GlobalMaxInactiveSeedingMinutes=1440 Session\ShareLimitAction=Remove Session\GlobalMaxRatio=1 Session\GlobalMaxSeedingMinutes=1440 Session\GlobalMaxInactiveSeedingMinutes=1440 Session\ShareLimitAction=Remove Session\GlobalMaxRatio=1 Session\GlobalMaxSeedingMinutes=1440 Session\GlobalMaxInactiveSeedingMinutes=1440 Session\ShareLimitAction=Remove # Root crontab entry: */15 * * * * /opt/-weight: 500;">docker/disk-guard.sh # Root crontab entry: */15 * * * * /opt/-weight: 500;">docker/disk-guard.sh # Root crontab entry: */15 * * * * /opt/-weight: 500;">docker/disk-guard.sh # View recent log entries tail -20 /var/log/disk-guard.log # Manual run -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check current disk usage df -h / /mnt/ssd # View recent log entries tail -20 /var/log/disk-guard.log # Manual run -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check current disk usage df -h / /mnt/ssd # View recent log entries tail -20 /var/log/disk-guard.log # Manual run -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check current disk usage df -h / /mnt/ssd # Identify the new drive lsblk # Create GPT partition table + single partition -weight: 600;">sudo fdisk /dev/sda # g (new GPT), n (new partition), 1, Enter, Enter, w (write) # Format as ext4 -weight: 600;">sudo mkfs.ext4 -L media-ssd /dev/sda1 # Create mount point and mount -weight: 600;">sudo mkdir -p /mnt/ssd -weight: 600;">sudo mount /dev/sda1 /mnt/ssd # Get UUID and add to fstab for persistent mounting UUID=$(-weight: 600;">sudo blkid -s UUID -o value /dev/sda1) echo "UUID=$UUID /mnt/ssd ext4 defaults 0 2" | -weight: 600;">sudo tee -a /etc/fstab # Identify the new drive lsblk # Create GPT partition table + single partition -weight: 600;">sudo fdisk /dev/sda # g (new GPT), n (new partition), 1, Enter, Enter, w (write) # Format as ext4 -weight: 600;">sudo mkfs.ext4 -L media-ssd /dev/sda1 # Create mount point and mount -weight: 600;">sudo mkdir -p /mnt/ssd -weight: 600;">sudo mount /dev/sda1 /mnt/ssd # Get UUID and add to fstab for persistent mounting UUID=$(-weight: 600;">sudo blkid -s UUID -o value /dev/sda1) echo "UUID=$UUID /mnt/ssd ext4 defaults 0 2" | -weight: 600;">sudo tee -a /etc/fstab # Identify the new drive lsblk # Create GPT partition table + single partition -weight: 600;">sudo fdisk /dev/sda # g (new GPT), n (new partition), 1, Enter, Enter, w (write) # Format as ext4 -weight: 600;">sudo mkfs.ext4 -L media-ssd /dev/sda1 # Create mount point and mount -weight: 600;">sudo mkdir -p /mnt/ssd -weight: 600;">sudo mount /dev/sda1 /mnt/ssd # Get UUID and add to fstab for persistent mounting UUID=$(-weight: 600;">sudo blkid -s UUID -o value /dev/sda1) echo "UUID=$UUID /mnt/ssd ext4 defaults 0 2" | -weight: 600;">sudo tee -a /etc/fstab # Create movies directory on the new drive -weight: 600;">sudo mkdir -p /mnt/ssd/movies -weight: 600;">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) -weight: 600;">sudo mount --bind /mnt/ssd/movies /mnt/media/movies # Persist in fstab echo "/mnt/ssd/movies /mnt/media/movies none bind 0 0" | -weight: 600;">sudo tee -a /etc/fstab # Restart Jellyfin to pick up the bind mount -weight: 500;">docker -weight: 500;">restart jellyfin # Create movies directory on the new drive -weight: 600;">sudo mkdir -p /mnt/ssd/movies -weight: 600;">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) -weight: 600;">sudo mount --bind /mnt/ssd/movies /mnt/media/movies # Persist in fstab echo "/mnt/ssd/movies /mnt/media/movies none bind 0 0" | -weight: 600;">sudo tee -a /etc/fstab # Restart Jellyfin to pick up the bind mount -weight: 500;">docker -weight: 500;">restart jellyfin # Create movies directory on the new drive -weight: 600;">sudo mkdir -p /mnt/ssd/movies -weight: 600;">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) -weight: 600;">sudo mount --bind /mnt/ssd/movies /mnt/media/movies # Persist in fstab echo "/mnt/ssd/movies /mnt/media/movies none bind 0 0" | -weight: 600;">sudo tee -a /etc/fstab # Restart Jellyfin to pick up the bind mount -weight: 500;">docker -weight: 500;">restart jellyfin -weight: 600;">sudo tune2fs -m 1 /dev/mapper/ubuntu--vg-ubuntu--lv -weight: 600;">sudo tune2fs -m 1 /dev/mapper/ubuntu--vg-ubuntu--lv -weight: 600;">sudo tune2fs -m 1 /dev/mapper/ubuntu--vg-ubuntu--lv -weight: 600;">sudo tailscale set --accept-dns=false -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved -weight: 600;">sudo tailscale set --accept-dns=false -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved -weight: 600;">sudo tailscale set --accept-dns=false -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved -weight: 500;">docker logs <container_name> # Check for permission errors — usually fix with: -weight: 600;">sudo chown -R 1000:1000 /opt/appdata/<container_name> -weight: 500;">docker logs <container_name> # Check for permission errors — usually fix with: -weight: 600;">sudo chown -R 1000:1000 /opt/appdata/<container_name> -weight: 500;">docker logs <container_name> # Check for permission errors — usually fix with: -weight: 600;">sudo chown -R 1000:1000 /opt/appdata/<container_name> # 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 -weight: 600;">sudo usermod -aG render $USER -weight: 600;">sudo usermod -aG video $USER # Log out/in, then -weight: 500;">restart Jellyfin container -weight: 500;">docker -weight: 500;">restart jellyfin # 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 -weight: 600;">sudo usermod -aG render $USER -weight: 600;">sudo usermod -aG video $USER # Log out/in, then -weight: 500;">restart Jellyfin container -weight: 500;">docker -weight: 500;">restart jellyfin # 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 -weight: 600;">sudo usermod -aG render $USER -weight: 600;">sudo usermod -aG video $USER # Log out/in, then -weight: 500;">restart Jellyfin container -weight: 500;">docker -weight: 500;">restart jellyfin # Install thermal management -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald # Check temperatures sensors # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y lm-sensors && -weight: 600;">sudo sensors-detect) # Install thermal management -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald # Check temperatures sensors # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y lm-sensors && -weight: 600;">sudo sensors-detect) # Install thermal management -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y thermald # Check temperatures sensors # (-weight: 500;">install with: -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y lm-sensors && -weight: 600;">sudo sensors-detect) # Verify Tailscale is connected tailscale -weight: 500;">status # Check if firewall is blocking -weight: 600;">sudo ufw -weight: 500;">status # If active, allow ports: -weight: 600;">sudo ufw allow 8096/tcp # Jellyfin -weight: 600;">sudo ufw allow 5055/tcp # Jellyseerr # (add other ports as needed) # Verify Tailscale is connected tailscale -weight: 500;">status # Check if firewall is blocking -weight: 600;">sudo ufw -weight: 500;">status # If active, allow ports: -weight: 600;">sudo ufw allow 8096/tcp # Jellyfin -weight: 600;">sudo ufw allow 5055/tcp # Jellyseerr # (add other ports as needed) # Verify Tailscale is connected tailscale -weight: 500;">status # Check if firewall is blocking -weight: 600;">sudo ufw -weight: 500;">status # If active, allow ports: -weight: 600;">sudo ufw allow 8096/tcp # Jellyfin -weight: 600;">sudo ufw allow 5055/tcp # Jellyseerr # (add other ports as needed) # Disable USB autosuspend echo -1 | -weight: 600;">sudo tee /sys/module/usbcore/parameters/autosuspend # Make permanent — add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub: # usbcore.autosuspend=-1 -weight: 600;">sudo -weight: 500;">update-grub -weight: 600;">sudo reboot # Disable USB autosuspend echo -1 | -weight: 600;">sudo tee /sys/module/usbcore/parameters/autosuspend # Make permanent — add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub: # usbcore.autosuspend=-1 -weight: 600;">sudo -weight: 500;">update-grub -weight: 600;">sudo reboot # Disable USB autosuspend echo -1 | -weight: 600;">sudo tee /sys/module/usbcore/parameters/autosuspend # Make permanent — add to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub: # usbcore.autosuspend=-1 -weight: 600;">sudo -weight: 500;">update-grub -weight: 600;">sudo reboot # Check disk usage df -h / /mnt/ssd # Run disk cleanup manually -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check what is eating space du -sh /mnt/media/downloads/torrents/ # Torrent downloads du -sh /var/lib/-weight: 500;">docker/ # Docker images/layers du -sh /var/lib/snapd/cache/ # Snap cache # If DNS is broken (Tailscale took over resolv.conf): -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved # Check disk-guard log tail -20 /var/log/disk-guard.log # Check disk usage df -h / /mnt/ssd # Run disk cleanup manually -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check what is eating space du -sh /mnt/media/downloads/torrents/ # Torrent downloads du -sh /var/lib/-weight: 500;">docker/ # Docker images/layers du -sh /var/lib/snapd/cache/ # Snap cache # If DNS is broken (Tailscale took over resolv.conf): -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved # Check disk-guard log tail -20 /var/log/disk-guard.log # Check disk usage df -h / /mnt/ssd # Run disk cleanup manually -weight: 600;">sudo /opt/-weight: 500;">docker/disk-guard.sh # Check what is eating space du -sh /mnt/media/downloads/torrents/ # Torrent downloads du -sh /var/lib/-weight: 500;">docker/ # Docker images/layers du -sh /var/lib/snapd/cache/ # Snap cache # If DNS is broken (Tailscale took over resolv.conf): -weight: 600;">sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart systemd-resolved # Check disk-guard log tail -20 /var/log/disk-guard.log # See all running containers -weight: 500;">docker ps # See all containers (including stopped) -weight: 500;">docker ps -a # View container logs -weight: 500;">docker logs <container_name> -weight: 500;">docker logs -f <container_name> # Follow (live) # Restart a container -weight: 500;">docker -weight: 500;">restart <container_name> # Stop / Start -weight: 500;">docker -weight: 500;">stop <container_name> -weight: 500;">docker -weight: 500;">start <container_name> # Remove a container (data in /opt/appdata is preserved) -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Update a container manually -weight: 500;">docker pull <image_name>:latest -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Then re-run the original -weight: 500;">docker run command # Check Docker disk usage -weight: 500;">docker system df # Clean up unused images/containers -weight: 500;">docker system prune -a # See all running containers -weight: 500;">docker ps # See all containers (including stopped) -weight: 500;">docker ps -a # View container logs -weight: 500;">docker logs <container_name> -weight: 500;">docker logs -f <container_name> # Follow (live) # Restart a container -weight: 500;">docker -weight: 500;">restart <container_name> # Stop / Start -weight: 500;">docker -weight: 500;">stop <container_name> -weight: 500;">docker -weight: 500;">start <container_name> # Remove a container (data in /opt/appdata is preserved) -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Update a container manually -weight: 500;">docker pull <image_name>:latest -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Then re-run the original -weight: 500;">docker run command # Check Docker disk usage -weight: 500;">docker system df # Clean up unused images/containers -weight: 500;">docker system prune -a # See all running containers -weight: 500;">docker ps # See all containers (including stopped) -weight: 500;">docker ps -a # View container logs -weight: 500;">docker logs <container_name> -weight: 500;">docker logs -f <container_name> # Follow (live) # Restart a container -weight: 500;">docker -weight: 500;">restart <container_name> # Stop / Start -weight: 500;">docker -weight: 500;">stop <container_name> -weight: 500;">docker -weight: 500;">start <container_name> # Remove a container (data in /opt/appdata is preserved) -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Update a container manually -weight: 500;">docker pull <image_name>:latest -weight: 500;">docker -weight: 500;">stop <container_name> && -weight: 500;">docker rm <container_name> # Then re-run the original -weight: 500;">docker run command # Check Docker disk usage -weight: 500;">docker system df # Clean up unused images/containers -weight: 500;">docker system prune -a - 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 - 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 - Lid close: Must -weight: 500;">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 - Set hostname to something memorable (e.g., mediaserver) - Create your user account (this guide assumes UID/GID 1000) - Connect to your network (Ethernet preferred) - Create an admin account on first visit - Select "Local" environment - 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) - 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 - 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) - 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: /media/movies - Enable "Rename Movies" - Enable "Use Hardlinks instead of Copy" -> YES (saves disk space when source and destination are on the same filesystem) - Settings -> Download Clients -> Add qBittorrent: Host: YOUR_IP Port: 8080 Category: radarr - Host: YOUR_IP - Category: radarr - Settings -> Quality Profiles -> Select "HD-1080p" as default - Settings -> Indexers -> (these sync automatically from Prowlarr) - 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) - Host: YOUR_IP - Category: radarr - Authentication in Settings -> General - Root Folder: /media/tv - Download Client: qBittorrent (category: sonarr) - Quality Profile: HD-1080p - Enable hardlinks - 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) - 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 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 - Click "Sync" — all indexers now appear in every *Arr app automatically - 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 - Create an admin account - Add media libraries: Movies -> /data/movies Shows -> /data/tv Music -> /data/music - Movies -> /data/movies - Shows -> /data/tv - Music -> /data/music - Set preferred language and metadata language - Movies -> /data/movies - Shows -> /data/tv - Music -> /data/music - 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 - Play a video in the Jellyfin web player - During playback, check if hardware transcoding is active: - 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) - Install the Nextcloud app from your phone's app store - Server: http://YOUR_IP:8088 (snap) or http://YOUR_IP:8443 (Docker) — use the Tailscale IP for remote access - Login with admin credentials - For photo auto-upload: Nextcloud app -> Auto Upload -> Enable - Libraries -> Add: Movies: Source /media/movies, Transcode cache /temp TV: Source /media/tv, Transcode cache /temp - Movies: Source /media/movies, Transcode cache /temp - TV: Source /media/tv, Transcode cache /temp - Plugins -> Use community plugin: Tdarr_Plugin_MC93_Migz1FFMPEG Target codec: HEVC (H.265) Use hardware: Intel QSV - 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 - Node -> Schedule -> Enable, set hours like 01:00-07:00 - Movies: Source /media/movies, Transcode cache /temp - TV: Source /media/tv, Transcode cache /temp - Target codec: HEVC (H.265) - Use hardware: Intel QSV - Node -> Schedule -> Enable, set hours like 01:00-07:00 - Settings -> Download Clients -> Add -> qBittorrent Host: YOUR_IP Port: 8080 Username/Password: your qBittorrent credentials Category: radarr (or sonarr, lidarr respectively) Test -> Save - Host: YOUR_IP - Username/Password: your qBittorrent credentials - Category: radarr (or sonarr, lidarr respectively) - Test -> Save - Host: YOUR_IP - Username/Password: your qBittorrent credentials - Category: radarr (or sonarr, lidarr respectively) - Test -> Save - 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 - 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 - Go to Jellyseerr -> Search for a movie -> Request it - Check Radarr -> should show the movie as "Searching" - Check qBittorrent -> should -weight: 500;">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 - 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 - removeCompletedDownloads: true — After import, -weight: 500;">remove torrent + delete source files - autoRedownloadFailed: true — Retry if download fails - Links to all services with their ports - System -weight: 500;">status overview - Quick-access to the most used services (Jellyfin, Jellyseerr, qBittorrent) - Optionally, an AI chat widget connected to your local Ollama instance - 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 - 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 -weight: 500;">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