feat(infra): add Jellyfin media server configuration and logging support

Add Jellyfin service to ansible inventory with hardware
transcoding and Casdoor SSO configuration. Configure
Alloy syslog listener to capture Jellyfin logs to Loki.
Update documentation with new service mapping and S3
bucket credential retrieval instructions.
This commit is contained in:
2026-05-04 15:33:25 -04:00
parent b9ce14ff77
commit f818b7917d
7 changed files with 392 additions and 6 deletions

149
ansible/jellyfin/README.md Normal file
View File

@@ -0,0 +1,149 @@
---
# Jellyfin Deployment for Ouranos
Jellyfin media server deployed on Rosalind Incus container.
## Overview
Jellyfin is an open-source media server for organizing, streaming, and managing media content. This deployment includes:
- Docker containerized deployment
- NVIDIA GPU passthrough for hardware-accelerated transcoding
- Prometheus metrics collection
- Syslog integration with Grafana Alloy
- Casdoor OIDC SSO support (via plugin)
## Deployment
### Prerequisites
1. Rosalind Incus container must be running with Docker installed
2. `/mnt/media` must be accessible from the Incus host
3. NVIDIA GPU must be passed through to the Rosalind container
4. Casdoor application must be configured for Jellyfin OIDC
### Installation
```bash
# From ansible directory
cd /home/robert/git/ouranos/ansible
# Deploy Jellyfin to Rosalind
ansible-playbook jellyfin/deploy.yml --limit rosalind.incus
```
### Updating
```bash
# Update Jellyfin container
ansible-playbook jellyfin/deploy.yml --limit rosalind.incus
```
## Configuration
### Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `jellyfin_user` | Service username | `jellyfin` |
| `jellyfin_group` | Service group name | `jellyfin` |
| `jellyfin_uid` | Service UID | `521` |
| `jellyfin_gid` | Service GID | `521` |
| `jellyfin_directory` | Base directory | `/srv/jellyfin` |
| `jellyfin_port` | HTTP port | `22086` |
| `jellyfin_syslog_port` | Syslog port | `51426` |
| `jellyfin_config_dir` | Config directory | `/srv/jellyfin/config` |
| `jellyfin_cache_dir` | Cache directory | `/srv/jellyfin/cache` |
| `jellyfin_media_dir` | Media bind mount | `/mnt/media` |
| `jellyfin_published_server_url` | External URL | `https://jellyfin.ouranos.helu.ca` |
### SSO Configuration
Jellyfin uses the `jellyfin-plugin-sso` community plugin for Casdoor OIDC authentication:
1. **Create Casdoor Application**:
- Application type: OIDC
- Callback URL: `https://jellyfin.ouranos.helu.ca/api/plugin/sso/callback`
- Enable PKCE
2. **Plugin Configuration**:
- Install manifest in `/config/plugins`
- Configure with Casdoor OIDC endpoints
3. **Casdoor Endpoints**:
- Authorization: `https://id.ouranos.helu.ca/oauth2/authorize`
- Token: `https://id.ouranos.helu.ca/oauth2/token`
- Userinfo: `https://id.ouranos.helu.ca/oauth2/userinfo`
## Monitoring
### Prometheus Metrics
Jellyfin exposes metrics at `http://localhost:8096/metrics`. These are collected by Prospero's Prometheus via:
- cAdvisor container metrics
- Process exporter
### Grafana Dashboard
Add a new data source in Grafana:
- Type: Prometheus
- URL: `http://prospero.incus:9090`
### Logs
View Jellyfin logs:
```bash
# Via Docker
docker logs -f jellyfin
# Via systemd
journalctl -u jellyfin -f
# Via Grafana Loki
https://loki.ouranos.helu.ca/explore?orgId=1&left=%5B%22now-1h%22,%22now%22,%22jellyfin%22,%7B%22job%22%3A%22jellyfin%22%7D%5D
```
## Troubleshooting
### Container won't start
```bash
# Check Docker status
docker ps -a | grep jellyfin
# Check logs
docker logs jellyfin
# Verify GPU passthrough
ls -la /dev/dri/
```
### Transcoding fails
1. Verify GPU is accessible: `nvidia-smi`
2. Check container has device access: `docker inspect jellyfin | grep Devices`
3. Review logs for transcoding errors
### SSO not working
1. Verify plugin is installed in `/config/plugins`
2. Check Casdoor application configuration
3. Verify redirect URLs match exactly
4. Browser console for OAuth errors
## Files
| Path | Description |
|------|-------------|
| `/srv/jellyfin/docker-compose.yml` | Generated Docker Compose config |
| `/etc/systemd/system/jellyfin.service` | Systemd wrapper service |
| `/srv/jellyfin/config` | Jellyfin configuration |
| `/srv/jellyfin/cache` | Transcode cache |
| `/srv/jellyfin/logs` | Application logs (via syslog) |
## References
- [Jellyfin Official Docs](https://jellyfin.org/docs/)
- [Jellyfin Docker Image](https://hub.docker.com/r/jellyfin/jellyfin)
- [SSO Plugin GitHub](https://github.com/9p4/jellyfin-plugin-sso)

107
ansible/jellyfin/deploy.yml Normal file
View File

@@ -0,0 +1,107 @@
---
- name: Deploy Jellyfin
hosts: ubuntu
become: true
vars:
ansible_python_interpreter: /usr/bin/python3
tasks:
- name: Check if host has jellyfin service
ansible.builtin.set_fact:
has_jellyfin: "{{ 'jellyfin' in services | default([]) }}"
- name: Skip hosts without jellyfin service
ansible.builtin.meta: end_host
when: not has_jellyfin
- name: Create jellyfin group
ansible.builtin.group:
name: "{{ jellyfin_group }}"
gid: "{{ jellyfin_gid }}"
- name: Create jellyfin user
ansible.builtin.user:
name: "{{ jellyfin_user }}"
comment: "Jellyfin service account"
group: "{{ jellyfin_group }}"
uid: "{{ jellyfin_uid }}"
home: "{{ jellyfin_directory }}"
system: true
shell: /bin/bash
- name: Add keeper_user to jellyfin group
ansible.builtin.user:
name: "{{ keeper_user }}"
groups: "{{ jellyfin_group }}"
append: true
- name: Create Jellyfin directories
ansible.builtin.file:
path: "{{ item }}"
owner: "{{ jellyfin_user }}"
group: "{{ jellyfin_group }}"
state: directory
mode: '0750'
loop:
- "{{ jellyfin_directory }}"
- "{{ jellyfin_config_dir }}"
- "{{ jellyfin_cache_dir }}"
- name: Check if Docker is installed
ansible.builtin.stat:
path: /var/run/docker.sock
register: docker_socket
- name: Deploy Docker Compose configuration
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ jellyfin_directory }}/docker-compose.yml"
owner: "{{ jellyfin_user }}"
group: "{{ jellyfin_group }}"
mode: '0644'
notify:
- Stop Jellyfin
- Pull Jellyfin image
- Start Jellyfin
- name: Create systemd service for Docker Compose
ansible.builtin.template:
src: jellyfin.service.j2
dest: /etc/systemd/system/jellyfin.service
mode: '0644'
notify:
- Reload systemd
- Enable Jellyfin
- Start Jellyfin
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Stop Jellyfin
ansible.builtin.command:
cmd: "docker compose down"
chdir: "{{ jellyfin_directory }}"
become_user: "{{ jellyfin_user }}"
become: true
- name: Pull Jellyfin image
ansible.builtin.command:
cmd: "docker compose pull"
chdir: "{{ jellyfin_directory }}"
become_user: "{{ jellyfin_user }}"
become: true
- name: Start Jellyfin
ansible.builtin.command:
cmd: "docker compose up -d"
chdir: "{{ jellyfin_directory }}"
become_user: "{{ jellyfin_user }}"
become: true
- name: Enable Jellyfin
ansible.builtin.systemd:
name: jellyfin
enabled: true
state: started
daemon_reload: true

View File

@@ -0,0 +1,34 @@
---
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
user: "{{ jellyfin_uid }}:{{ jellyfin_gid }}"
ports:
- "{{ jellyfin_port }}:8096/tcp"
- "7359:7359/udp"
volumes:
- "{{ jellyfin_config_dir }}:/config"
- "{{ jellyfin_cache_dir }}:/cache"
- "{{ jellyfin_media_dir }}:/media:ro"
restart: unless-stopped
devices:
- /dev/dri:/dev/dri
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8096/dashboard"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: syslog
options:
syslog-address: "udp://prospero.incus:1514"
tag: "jellyfin"
environment:
- PUID={{ jellyfin_uid }}
- PGID={{ jellyfin_gid }}
- TZ=America/Toronto
- JELLYFIN_PublishedServerUrl={{ jellyfin_published_server_url }}
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,18 @@
---
[Unit]
Description=Jellyfin Docker Compose Service
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ jellyfin_directory }}
User={{ jellyfin_user }}
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target