TL;DR: Putting Traefik in front and allow only specific IPs to reach your media server. You get valid HTTPS, clean app logins, and a tiny public attack surface and took me like 5 min.
Why this approach?
- I’m lazy and I only give this type of stuff out to people I can trust.
- Also it just works.
What you’ll build
- Traefik terminates HTTPS on ports 80/443.
- Only your allow-listed IPs can reach
media.example.com
. - Jellyfin/Emby still shows its normal sign-in (no SSO).
- No publicly exposed admin dashboards.
Prereqs
- Docker & Docker Compose installed.
- A domain with a DNS
A
/AAAA
record (e.g.,media.example.com
) pointing to your box. - Port-forward 80/tcp and 443/tcp from your router to the Traefik host (do not forward the Jellyfin/Emby app port).
1) Create the shared network
docker network create traefik-public
2) Prepare Let’s Encrypt storage
mkdir -p ./letsencrypt
touch ./letsencrypt/acme.json
chmod 600 ./letsencrypt/acme.json
3) Docker Compose
Replace:
media.example.com
with your domainadmin@example.com
with your ACME email- The CIDRs under
ipallowlist.sourcerange
with your approved IPs (use IPv4/IPv6 as needed)
version: "3.8"
networks:
traefik-public:
external: true # run: docker network create traefik-public
services:
traefik:
image: traefik:v3.4
container_name: traefik
restart: unless-stopped
command:
- "--log.level=INFO"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Dashboard/API in "secure" mode only (we'll route it with a whitelist below)
- "--api.dashboard=true"
# Let's Encrypt (HTTP challenge via entrypoint web:80)
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- traefik-public
labels:
- "traefik.enable=true"
# (Optional) Secure dashboard at https://traefik.example.com (allow-listed)
- "traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=allow-my-ips@docker"
# A tiny helper container that only exists to define reusable middlewares.
allowlist:
image: alpine:latest
container_name: allowlist
command: ["sleep", "infinity"]
restart: unless-stopped
labels:
- "traefik.enable=true"
# 👇 Replace with your actual permitted IPs or CIDRs (examples below are RFC TEST-NET)
- "traefik.http.middlewares.allow-my-ips.ipallowlist.sourcerange=203.0.113.0/24,198.51.100.0/24"
# Use emby/embyserver:latest or swap to jellyfin/jellyfin:latest — labels are the same idea.
media:
image: emby/embyserver:latest
container_name: media
restart: unless-stopped
networks:
- traefik-public
volumes:
- "/path/to/your/media:/media:ro"
# Optional local access without TLS (LAN only). Remove if you don't need it.
ports:
- "8096:8096"
labels:
- "traefik.enable=true"
# HTTP -> HTTPS redirect
- "traefik.http.routers.media-http.rule=Host(`media.example.com`)"
- "traefik.http.routers.media-http.entrypoints=web"
- "traefik.http.routers.media-http.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# HTTPS router with IP allow-list and Let's Encrypt
- "traefik.http.routers.media-https.rule=Host(`media.example.com`)"
- "traefik.http.routers.media-https.entrypoints=websecure"
- "traefik.http.routers.media-https.tls=true"
- "traefik.http.routers.media-https.tls.certresolver=letsencrypt"
- "traefik.http.routers.media-https.middlewares=allow-my-ips@docker"
# Service port (Emby/Jellyfin inside the container)
- "traefik.http.services.media.loadbalancer.server.port=8096"
4) DNS & router
- Point
media.example.com
→ your WAN IP (DNS “A” record; add AAAA for IPv6 if you’ll allow-list v6). - Forward 80 and 443 to the Traefik host. Do not expose 8096 directly to the internet.
5) Start it up
docker compose up -d
Visit https://media.example.com
from an allow-listed IP. You should see the Jellyfin/Emby sign-in as usual.
Managing the allow-list
- Edit the middleware label on the
allowlist
container:traefik.http.middlewares.allow-my-ips.ipallowlist.sourcerange=203.0.113.0/24,198.51.100.10/32
…thendocker compose up -d
to apply. You can list as many CIDRs and single IPs as you need.- I also have portainer so when im on the move i just vpn back home and modify the env an then restart it and boom done.
- Traveling a lot or on mobile data with changing IPs? This strategy gets annoying. At that point, a tunnel/VPN (Tailscale, etc.) is actually the ergonomic answer—even if you don’t want it today.
Optional hardening
- Rate limiting to blunt brute-force and scrapers (Traefik
RateLimit
middleware). - CrowdSec / WAF at the edge if you later move away from strict allow-listing.
- Separate dashboard hostname (
traefik.example.com
) and keep it on the sameallow-my-ips
middleware (already in the compose). - HSTS and modern TLS ciphers (Traefik supports custom TLS options).