Quick Reference Sheet
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): Rungetent group render | cut -d: -f3on 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
/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
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 namedcard0andrenderD128, 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.
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
}
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.
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.