jellyfin-media/docker-compose.yml

229 lines
6.5 KiB
YAML

version: "3.9"
services:
# Jellyfin Media Server (Plex alternative - no license needed)
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
volumes:
- ./config/jellyfin:/config
- ./cache/jellyfin:/cache
- /mnt/r2-media:/media:ro # R2 mounted via rclone
ports:
- "8096:8096"
- "8920:8920" # HTTPS
networks:
- media-network
depends_on:
- r2-mount
labels:
- "traefik.enable=true"
# HTTP router (redirects to HTTPS)
- "traefik.http.routers.jellyfin.rule=Host(`movies.jeffemmett.com`)"
- "traefik.http.routers.jellyfin.entrypoints=web"
- "traefik.http.routers.jellyfin.middlewares=jellyfin-https-redirect"
# HTTPS router
- "traefik.http.routers.jellyfin-secure.rule=Host(`movies.jeffemmett.com`)"
- "traefik.http.routers.jellyfin-secure.entrypoints=websecure"
- "traefik.http.routers.jellyfin-secure.tls.certresolver=letsencrypt"
- "traefik.http.routers.jellyfin-secure.middlewares=jellyfin-headers"
# Service
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
# Middleware: HTTPS redirect
- "traefik.http.middlewares.jellyfin-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.jellyfin-https-redirect.redirectscheme.permanent=true"
# Middleware: Headers for proper session handling
- "traefik.http.middlewares.jellyfin-headers.headers.customRequestHeaders.X-Forwarded-Proto=https"
- "traefik.http.middlewares.jellyfin-headers.headers.customRequestHeaders.X-Forwarded-Host=movies.jeffemmett.com"
- "traefik.http.middlewares.jellyfin-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.jellyfin-headers.headers.stsIncludeSubdomains=true"
# R2 Mount Service (rclone FUSE mount)
r2-mount:
image: rclone/rclone:latest
container_name: r2-mount
restart: unless-stopped
cap_add:
- SYS_ADMIN
devices:
- /dev/fuse
security_opt:
- apparmor:unconfined
environment:
- RCLONE_CONFIG=/config/rclone/rclone.conf
volumes:
- ./config/rclone:/config/rclone:ro
- /mnt/r2-media:/mnt/r2-media:shared
- ./cache/rclone:/cache
command: >
mount r2:plex-media /mnt/r2-media
--allow-other
--allow-non-empty
--vfs-cache-mode full
--vfs-cache-max-size 50G
--vfs-cache-max-age 72h
--vfs-read-chunk-size 128M
--vfs-read-chunk-size-limit 1G
--buffer-size 512M
--dir-cache-time 72h
--poll-interval 15s
--log-level INFO
--cache-dir /cache
networks:
- media-network
# Sonarr - TV Show Management
sonarr:
image: linuxserver/sonarr:latest
container_name: sonarr
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
volumes:
- ./config/sonarr:/config
- /mnt/r2-media/tv:/tv
- ./downloads:/downloads
ports:
- "8989:8989"
networks:
- media-network
depends_on:
- r2-mount
# Radarr - Movie Management
radarr:
image: linuxserver/radarr:latest
container_name: radarr
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
volumes:
- ./config/radarr:/config
- /mnt/r2-media/movies:/movies
- ./downloads:/downloads
ports:
- "7878:7878"
networks:
- media-network
depends_on:
- r2-mount
# Prowlarr - Indexer Management
prowlarr:
image: linuxserver/prowlarr:latest
container_name: prowlarr
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
volumes:
- ./config/prowlarr:/config
ports:
- "9696:9696"
networks:
- media-network
# Transmission - Download Client
transmission:
image: linuxserver/transmission:latest
container_name: transmission
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- TRANSMISSION_WEB_HOME=/web
volumes:
- ./config/transmission:/config
- ./downloads:/downloads
- ./watch:/watch
ports:
- "9091:9091"
- "51413:51413"
- "51413:51413/udp"
networks:
- media-network
# R2 Sync Service - Moves completed downloads to R2
r2-sync:
build:
context: ./services/r2-sync
dockerfile: Dockerfile
container_name: r2-sync
restart: unless-stopped
environment:
- R2_ACCOUNT_ID=${R2_ACCOUNT_ID}
- R2_ACCESS_KEY=${R2_ACCESS_KEY}
- R2_SECRET_KEY=${R2_SECRET_KEY}
- R2_BUCKET=plex-media
- WATCH_DIR=/downloads/complete
- SYNC_INTERVAL=300
volumes:
- ./downloads/complete:/downloads/complete:ro
- ./config/rclone:/config/rclone:ro
- ./logs/r2-sync:/logs
networks:
- media-network
# Cost Monitor - Tracks R2 storage costs and exposes Prometheus metrics
cost-monitor:
build:
context: ./services/cost-monitor
dockerfile: Dockerfile
container_name: cost-monitor
restart: unless-stopped
environment:
- R2_BUCKET=plex-media
- RCLONE_CONFIG=/config/rclone/rclone.conf
- METRICS_PORT=9100
- UPDATE_INTERVAL=3600
volumes:
- ./config/rclone:/config/rclone:ro
ports:
- "9100:9100"
networks:
- media-network
# Traefik Reverse Proxy
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=jeff@jeffemmett.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik/letsencrypt:/letsencrypt
networks:
- media-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.jeffemmett.com`)"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.service=api@internal"
networks:
media-network:
driver: bridge