From b76c21039098f4c3eb6cb3f6ebfe72781b08b405 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 18 Apr 2025 21:13:55 +0200 Subject: [PATCH] Include jellyfin provisioning --- playbook.yml | 5 +++ tasks/jellyfin.yml | 66 ++++++++++++++++++++++++++++ tasks/wireguard_media.yml | 4 +- templates/jellyfin/docker-compose.j2 | 44 +++++++++++++++++++ templates/jellyfin/nginx.j2 | 57 ++++++++++++++++++++++++ templates/network/hosts.j2 | 1 + templates/nftables.j2 | 6 ++- vars/jellyfin.yml | 5 +++ vars/network.yml | 8 ++-- vars/vpn_media.yml | 3 ++ 10 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 tasks/jellyfin.yml create mode 100644 templates/jellyfin/docker-compose.j2 create mode 100644 templates/jellyfin/nginx.j2 create mode 100644 vars/jellyfin.yml diff --git a/playbook.yml b/playbook.yml index f171dcb..2d64e94 100644 --- a/playbook.yml +++ b/playbook.yml @@ -48,6 +48,10 @@ ansible.builtin.import_tasks: 'tasks/nginx.yml' tags: nginx + - name: Jellyfin provisioning + ansible.builtin.import_tasks: 'tasks/jellyfin.yml' + tags: jellyfin + handlers: - name: Import handlers ansible.builtin.import_tasks: 'handlers.yml' @@ -60,3 +64,4 @@ - 'vars/syncthing.yml' - 'vars/mpd.yml' - 'vars/radicale.yml' + - 'vars/jellyfin.yml' diff --git a/tasks/jellyfin.yml b/tasks/jellyfin.yml new file mode 100644 index 0000000..68c185a --- /dev/null +++ b/tasks/jellyfin.yml @@ -0,0 +1,66 @@ +- name: Create directories + become: true + ansible.builtin.file: + path: '{{ item.path }}' + state: directory + owner: '{{ item.owner }}' + group: '{{ item.group }}' + mode: '0755' + loop: + - path: '{{ jellyfin_configuration_dir }}' + owner: sonny + group: sonny + + - path: '{{ jellyfin_media_dir }}' + owner: sonny + group: sonny + + - path: '{{ jellyfin_cache_dir }}' + owner: sonny + group: sonny + + - path: '{{ jellyfin_app_dir }}' + owner: root + group: root + + - path: '{{ jellyfin_app_dir }}/nginx.conf.d' + owner: sonny + group: sonny + +- name: Copy docker-compose file + become: true + ansible.builtin.template: + src: templates/jellyfin/docker-compose.j2 + dest: '{{ jellyfin_app_dir }}/docker-compose.yml' + owner: sonny + group: sonny + mode: '0755' + +- name: Copy NGINX configuration + become: true + ansible.builtin.template: + src: 'templates/jellyfin/nginx.j2' + dest: '{{ jellyfin_app_dir }}/nginx.conf.d/default.conf' + owner: sonny + group: sonny + mode: '0755' + +- name: Stop jellyfin + community.docker.docker_compose_v2: + project_src: '{{ jellyfin_app_dir }}' + state: stopped + +- name: Pull {{ image_tag }} + community.docker.docker_compose_v2: + project_src: '{{ jellyfin_app_dir }}' + pull: missing + +- name: Remove dangling containers + community.docker.docker_compose_v2: + project_src: '{{ jellyfin_app_dir }}' + remove_orphans: true + +- name: Start jellyfin + community.docker.docker_compose_v2: + project_src: '{{ jellyfin_app_dir }}' + state: present diff --git a/tasks/wireguard_media.yml b/tasks/wireguard_media.yml index 17d9b26..c21ea72 100644 --- a/tasks/wireguard_media.yml +++ b/tasks/wireguard_media.yml @@ -82,9 +82,9 @@ owner: '{{ ansible_user_id }}' loop: - src: 'templates/network/wireguard/media/mobile_1.wireguard.j2' - dest: '/tmp/mobile_1.conf' + dest: '/tmp/pixel.conf' - src: 'templates/network/wireguard/media/mobile_2.wireguard.j2' - dest: '/tmp/mobile_2.conf' + dest: '/tmp/mobile_mam.conf' - src: 'templates/network/wireguard/media/tv.wireguard.j2' dest: '/tmp/tv.conf' when: copy_vpn_media_configurations diff --git a/templates/jellyfin/docker-compose.j2 b/templates/jellyfin/docker-compose.j2 new file mode 100644 index 0000000..df16170 --- /dev/null +++ b/templates/jellyfin/docker-compose.j2 @@ -0,0 +1,44 @@ +# {{ ansible_managed }} +# + +networks: + jellyfin-net: + ipam: + config: + - subnet: '{{ jellyfin_subnet }}' + +services: + jellyfin: + image: {{ jellyfin_image_tag }} + container_name: jellyfin + user: {{ ansible_user_uid }}:{{ ansible_user_gid }} + group_add: + - 44 # video group + - 105 # render group + volumes: + - {{ jellyfin_configuration_dir }}:/config + - {{ jellyfin_cache_dir }}:/cache + - type: bind + source: {{ jellyfin_media_dir }} + target: /media + - /etc/passwd:/etc/passwd:ro + - /etc/group:/etc/group:ro + restart: always + networks: + jellyfin-net: + expose: + - {{ jellyfin_web_port }}/tcp + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 + - /dev/dri/card0:/dev/dri/card0 + + nginx: + image: nginx:mainline-alpine + depends_on: + - jellyfin + restart: always + networks: + jellyfin-net: + ipv4_address: '{{ jellyfin_nginx_ip }}' + volumes: + - '{{ jellyfin_app_dir }}/nginx.conf.d:/etc/nginx/conf.d' diff --git a/templates/jellyfin/nginx.j2 b/templates/jellyfin/nginx.j2 new file mode 100644 index 0000000..56917f3 --- /dev/null +++ b/templates/jellyfin/nginx.j2 @@ -0,0 +1,57 @@ +# {{ ansible_managed }} + +upstream jellyfin-upstream { + server jellyfin:{{ jellyfin_web_port }}; +} + +server { + listen 80; + server_name {{ jellyfin_domain }}; + http2 on; + + ## The default `client_max_body_size` is 1M, this might not be enough for some posters, etc. + client_max_body_size 20M; + + # Comment next line to allow TLSv1.0 and TLSv1.1 if you have very old clients + ssl_protocols TLSv1.3 TLSv1.2; + + # Security / XSS Mitigation Headers + add_header X-Content-Type-Options "nosniff"; + + # Permissions policy. May cause issues with some clients + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), display-capture=(), document-domain=(), encrypted-media=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), local-fonts=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=(), serial=(), sync-xhr=(), usb=(), xr-spatial-tracking=()" always; + + # Content Security Policy + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + # Enforces https content and restricts JS/CSS to origin + # External Javascript (such as cast_sender.js for Chromecast) must be whitelisted. + add_header Content-Security-Policy "default-src http: data: blob: ; img-src 'self' http://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self'"; + + location / { + # Proxy main Jellyfin traffic + proxy_pass http://jellyfin-upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + + # Disable buffering when the nginx proxy gets very resource heavy upon streaming + proxy_buffering off; + } + + location /socket { + # Proxy Jellyfin Websockets traffic + proxy_pass http://jellyfin-upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + } +} diff --git a/templates/network/hosts.j2 b/templates/network/hosts.j2 index 13dca81..a5392ee 100644 --- a/templates/network/hosts.j2 +++ b/templates/network/hosts.j2 @@ -8,3 +8,4 @@ {{ transmission_nginx_ip }} {{ transmission_domain }} {{ syncthing_nginx_ip }} {{ syncthing_domain }} {{ radicale_nginx_ip }} {{ radicale_domain }} +{{ jellyfin_nginx_ip }} {{ jellyfin_domain }} diff --git a/templates/nftables.j2 b/templates/nftables.j2 index 2e214ca..f1f4429 100644 --- a/templates/nftables.j2 +++ b/templates/nftables.j2 @@ -52,7 +52,7 @@ table ip filter { chain vpn_chain { meta l4proto { tcp, udp } th dport 53 ip saddr . ip daddr @vpn_set accept comment "DNS" - tcp dport { {{ http_port }}, {{ https_port }} } ip saddr . ip daddr @vpn_set accept comment "HTTP/HTTPS" + tcp dport { {{ http_port }}, {{ https_port }} } ip saddr . ip daddr @vpn_set accept comment "HTTP/HTTPS" # TODO: remove? tcp dport 80 ip saddr {{ vpn_subnet }} ip daddr {{ transmission_nginx_ip }} accept comment "Transmission Web" @@ -75,7 +75,7 @@ table ip filter { chain media_vpn_chain { meta l4proto { tcp, udp } th dport 53 ip saddr . ip daddr @vpn_media_set accept comment "DNS" - tcp dport {{ jellyfin_http_port }} ip saddr . ip daddr @vpn_media_set accept comment "Jellyfin HTTP" + tcp dport { 80, 443 } ip saddr {{ vpn_media_subnet }} ip daddr {{ jellyfin_nginx_ip }} accept comment "Jellyfin" } # docker's user configurable forward hook chain @@ -83,5 +83,7 @@ table ip filter { iifname {{ vpn_interface }} ip saddr {{ vpn_subnet }} ip daddr {{ transmission_nginx_ip }} accept iifname {{ vpn_interface }} ip saddr {{ vpn_subnet }} ip daddr {{ syncthing_nginx_ip }} accept iifname {{ vpn_interface }} ip saddr {{ vpn_subnet }} ip daddr {{ radicale_nginx_ip }} accept + + iifname {{ vpn_media_interface }} ip saddr {{ vpn_media_subnet }} ip daddr {{ jellyfin_nginx_ip }} accept } } diff --git a/vars/jellyfin.yml b/vars/jellyfin.yml new file mode 100644 index 0000000..03901e6 --- /dev/null +++ b/vars/jellyfin.yml @@ -0,0 +1,5 @@ +jellyfin_image_tag: jellyfin/jellyfin:10.10.4 +jellyfin_app_dir: /srv/docker/jellyfin +jellyfin_configuration_dir: /home/sonny/.config/jellyfin +jellyfin_media_dir: /home/sonny/media/video +jellyfin_cache_dir: /home/sonny/media/cache diff --git a/vars/network.yml b/vars/network.yml index 83e09bf..e508932 100644 --- a/vars/network.yml +++ b/vars/network.yml @@ -69,6 +69,8 @@ transmission_web_port: 9091 transmission_peer_port: 51413 transmission_nginx_ip: '172.16.238.10' -jellyfin_http_port: 8096 -jellyfin_service_port: 1900 -jellyfin_client_port: 7359 +jellyfin_domain: 'jellyfin.{{ domain_name }}' +jellyfin_prefix: 24 +jellyfin_subnet: '172.8.238.0/{{ transmission_prefix }}' +jellyfin_web_port: 8096 +jellyfin_nginx_ip: '172.8.238.10' diff --git a/vars/vpn_media.yml b/vars/vpn_media.yml index 82f7634..df89a1d 100644 --- a/vars/vpn_media.yml +++ b/vars/vpn_media.yml @@ -21,6 +21,7 @@ vpn_media_peers: ip: '10.0.1.4' allowed_ips: - '{{ vpn_media_subnet }}' + - '{{ jellyfin_subnet }}' public_key: '6fj8FXvzT0IUlZLJjQ/+FhwwRDsJeQsUFHqKQcyXdwQ=' preshared_key_path: '{{ vpn_config_dir }}/keys/private/preshared-media-mobile-1.psk' preshared_key_source_path: 'files/wireguard/media/preshared-mobile-1.psk' @@ -30,6 +31,7 @@ vpn_media_peers: ip: '10.0.1.5' allowed_ips: - '{{ vpn_media_subnet }}' + - '{{ jellyfin_subnet }}' public_key: 'w/pswNrAYFdEUoaLk3zSqOu4gg2s41BBCN02E//ai1c=' preshared_key_path: '{{ vpn_config_dir }}/keys/private/preshared-media-mobile-2.psk' preshared_key_source_path: 'files/wireguard/media/preshared-mobile-2.psk' @@ -39,6 +41,7 @@ vpn_media_peers: ip: '10.0.1.6' allowed_ips: - '{{ vpn_media_subnet }}' + - '{{ jellyfin_subnet }}' public_key: '5+yz9C9PhaLhsvAZ1e3mDsTQpMZVrPZnSQa6ERJIKU0=' preshared_key_path: '{{ vpn_config_dir }}/keys/private/preshared-media-tv.psk' preshared_key_source_path: 'files/wireguard/media/preshared-tv.psk'