home / blog / media streaming ← homelab cinema club

Self-Hosting & Media

Building a Self-Hosted Media Cloud: GPU Transcoding and Reverse Proxy Deployments (CineVault Architecture)

Published: July 12, 2026 • 9 min read · ← Back to Blog

Quick Reference Sheet

What it is for:
Enabling real-time video encoding hardware access inside containers and restricting dashboard exposure to secure networks.
Who uses it:
Homelab Operators, Media Engineers, SysAdmins, and Privacy Advocates building custom entertainment clouds.
Where it is useful:
Deploying Jellyfin/Plex servers, optimizing video transcoding on low-power host CPUs, and hardening admin portals with whitelisting.
Command Snippet Notice: You can copy configuration code blocks throughout this article using the Copy button. Always replace placeholders with your actual device details:
How to find the parameters for this guide:
  • media.your-domain.com: Replace this with your actual registered domain or subdomain pointing to your host server's public IP address.
  • 192.168.0.0/16 / 10.0.0.0/8: These are your trusted local IP ranges. If your router uses a different subnet (e.g. 172.16.x.x), update this range in the Caddyfile.
  • 104 (GPU render group ID): Run getent group render | cut -d: -f3 on your host terminal to find the exact ID mapping for GPU hardware render blocks.

Deploying a private media library (like Jellyfin or Plex) on a home lab server is a common project. However, streaming high-bitrate 4K content to a phone or tablet outside your local network requires real-time hardware transcoding to prevent buffering and avoid pegging the host CPU at 100% utilization. This guide details how to configure GPU passthrough within Docker containers and secure administrative interfaces using reverse proxies.

1. Enabling GPU Device Passthrough in Docker

By default, Docker containers cannot communicate with host hardware accelerators. To resolve this, map the host GPU interfaces inside your docker-compose.yml service configurations.

For Intel Graphics (Intel QuickSync - QSV)

On Intel-based systems, map the Direct Rendering Infrastructure (DRI) character devices (/dev/dri):

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: mediaserver-core
    devices:
      - /dev/dri:/dev/dri
    # Uncomment + set YOUR render GID (run: getent group render)
    # group_add:
    #   - "104"
    volumes:
      - /opt/jellyfin/config:/config
      - /opt/jellyfin/cache:/cache
      - /DATA/media:/data
    networks:
      - media-net
    restart: always

networks:
  media-net:
    driver: bridge
Caution: GPU Device Access
Mapping host hardware devices directly (/dev/dri) exposes the host graphics kernel driver stack to the container. If the host GPU driver suffers from unpatched vulnerabilities, local root users inside the container can trigger host-level kernel panics or execute arbitrary code outside the container boundary. Keep your host Linux kernel updated.

For NVIDIA Graphics (NVENC)

On NVIDIA systems, install the NVIDIA Container Toolkit on your host OS and expose the GPU capabilities inside the docker compose deployment reservations:

services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: mediaserver-core
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    volumes:
      - /opt/jellyfin/config:/config
      - /opt/jellyfin/cache:/cache
      - /DATA/media:/data
    networks:
      - media-net
    restart: always

networks:
  media-net:
    driver: bridge
Troubleshooting: NVIDIA GPU Transcoding fails inside container
Workaround: Make sure the official NVIDIA Container Toolkit is installed on the host OS. Without it, the Docker daemon cannot hook into the graphics driver kernel. Verify it exists by executing nvidia-container-cli -V on your host machine.

How to Identify Your Host GPU & Driver Status

Before writing your Docker configurations, run these diagnostic commands on your host Linux terminal to identify your hardware and verify driver presence:

  • Check physical hardware controllers:
    lspci | grep -Ei 'vga|3d|display'
    This command lists all graphics cards installed on your motherboard (e.g., *Intel Corporation UHD Graphics* or *NVIDIA Corporation GA106*).
  • For Intel QuickSync (QSV): Verify that Direct Rendering Infrastructure kernel nodes exist:
    ls -la /dev/dri
    If you see character devices named card0 and renderD128, your Intel integrated graphics drivers are active and ready.
  • For NVIDIA (NVENC): Verify that the official NVIDIA proprietary drivers are installed and running on the host system:
    nvidia-smi
    If this prints a status screen showing your GPU model name, active memory allocation, and CUDA version, your driver stack is fully functional.

2. Verifying Transcoding Inside the Container

Once the container is running, verify that the card is accessible by executing rendering commands inside the container namespace:

# For Intel devices, check if rendering nodes are visible
docker exec -it mediaserver-core ls -la /dev/dri

# Run ffmpeg test to ensure encoder codecs are registered
docker exec -it mediaserver-core jellyfin-ffmpeg -decoders | grep qsv

If the terminal outputs available QSV codecs (like h264_qsv or hevc_qsv), hardware acceleration is configured successfully.

Troubleshooting: Jellyfin fails to play videos (Permission Denied)
Workaround: Run docker exec -it mediaserver-core ls -l /dev/dri. If it is only readable by root, you must bind your host's render group ID to the container. Add group_add: - "104" (replace with your host's actual `render` group ID found using getent group render) under your Jellyfin service parameters in your docker-compose.yml file.

3. Securing Access with Caddy Reverse Proxy

Avoid exposing port 8096 directly. Setup a reverse proxy like Caddy to manage automatic SSL certification via Let's Encrypt, block public access to sensitive administrative dashboards, and route client traffic securely.

Create a Caddyfile on your host server to route requests securely:

# Run Caddy in Docker on the SAME media-net network as Jellyfin,
# otherwise the mediaserver-core hostname will not resolve.
# (docker-compose.yml for caddy: image caddy:2-alpine, volumes for
#  ./Caddyfile:/etc/caddy/Caddyfile + caddy_data, networks: [media-net])
media.your-domain.com {
    # Route all traffic to the container
    reverse_proxy mediaserver-core:8096

    # Jellyfin's dashboard is a client-side route: the part after #
    # in /web/index.html#!/dashboard never reaches the server, so it
    # cannot be matched here. Restrict the whole /web app to LAN instead.
    @admin_paths {
        path /web/*
        not remote_ip 192.168.0.0/16 10.0.0.0/8
    }

    # Deny non-local access attempts with a 403 Forbidden status
    respond @admin_paths "Access Denied: Admin paths restricted to local network." 403
}
Caution: HTTP Header Spoofing
The remote_ip filter rules inside Caddy depend on trust boundaries. If your Caddy server is deployed behind Cloudflare, AWS ALB, or another upstream load balancer, clients can easily spoof their IP address by injecting custom X-Forwarded-For HTTP headers. In proxy chains, configure the trusted_proxies parameter inside Caddy to ensure headers are stripped.

This allows external users to watch media streams securely, but prevents anyone outside your private network from accessing administrative consoles or modifying server configurations.

Troubleshooting: Whitelist blocks local administrator connections
Workaround: If you lose admin access on your local computer, verify that your local subnet matches the whitelisted ranges (e.g. 192.168.0.0/16). If your router uses a different range (such as 172.16.x.x or 10.x.x.x), update the Caddyfile remote_ip block to include your subnet.

Related Reads

“Content is king.”

— Bill Gates