Skip to content

Commit 7c2146b

Browse files
shanemcdclaude
andcommitted
Add Jellyfin media server role as rootless Podman quadlet
Deploys Jellyfin as a user-scoped systemd service via a quadlet .container file, with configurable ports, media path (defaults to ~/media), and podman auto-update support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20da57c commit 7c2146b

File tree

7 files changed

+129
-0
lines changed

7 files changed

+129
-0
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ uvx --from ansible-core ansible-playbook shanemcd.toolbox.<playbook_name>
6262
- `inception` - Meta-playbook for full system setup (runs oh_my_zsh, dotfiles, flatpaks, fonts, emacs; requires `--ask-become-pass` or `-K`)
6363
- `sunshine` - Configure Sunshine game streaming with keybindings and enable systemd user service
6464
- `nfs` - Configure NFS server for media sharing (requires `--ask-become-pass` or `-K`)
65+
- `jellyfin` - Deploy Jellyfin media server as a rootless Podman quadlet (user-scoped systemd service)
6566

6667
## Adding New Playbooks and Roles
6768

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,26 @@ ansible-playbook shanemcd.toolbox.jetkvm_tailscale \
167167
2. Installs tailscaled daemon with init script (`S22tailscale`)
168168
3. Configures persistent state in `/userdata/tailscale-state`
169169
4. Authenticates using provided auth key
170+
171+
### Services
172+
173+
#### `jellyfin`
174+
Deploy Jellyfin media server as a rootless Podman container managed by a user-scoped systemd quadlet.
175+
176+
```bash
177+
ansible-playbook shanemcd.toolbox.jellyfin -v
178+
```
179+
180+
**What it does:**
181+
1. Creates `~/media` directory (shared with the NFS role)
182+
2. Deploys a quadlet `.container` file to `~/.config/containers/systemd/`
183+
3. Enables `loginctl linger` so the service runs without an active login session
184+
4. Starts and enables the `jellyfin` systemd user service
185+
186+
**Options:**
187+
- `jellyfin_image` - Container image (default: `docker.io/jellyfin/jellyfin:latest`)
188+
- `jellyfin_web_port` - HTTP port (default: `8096`)
189+
- `jellyfin_media_path` - Media library path (default: `~/media`)
190+
- `jellyfin_media_readonly` - Mount media read-only (default: `true`)
191+
- `jellyfin_auto_update` - Enable `podman auto-update` (default: `true`)
192+
- `jellyfin_enable_linger` - Enable loginctl linger (default: `true`)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
- name: Deploy Jellyfin media server as a Podman quadlet
3+
hosts: localhost
4+
connection: local
5+
gather_facts: true
6+
7+
roles:
8+
- jellyfin
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
# Container image
3+
jellyfin_image: docker.io/jellyfin/jellyfin:latest
4+
5+
# Enable podman auto-update for the container
6+
jellyfin_auto_update: true
7+
8+
# Web UI port (HTTP)
9+
jellyfin_web_port: 8096
10+
11+
# Optional ports (set to a port number to enable)
12+
# jellyfin_https_port: 8920
13+
# jellyfin_discovery_port_dlna: 1900
14+
# jellyfin_discovery_port_client: 7359
15+
16+
# Media library path on the host
17+
jellyfin_media_path: "{{ ansible_facts['env']['HOME'] }}/media"
18+
19+
# Mount media as read-only
20+
jellyfin_media_readonly: true
21+
22+
# Named Podman volumes for persistent data
23+
jellyfin_config_volume: jellyfin-config
24+
jellyfin_cache_volume: jellyfin-cache
25+
26+
# Enable loginctl linger so the user service runs without an active login session
27+
jellyfin_enable_linger: true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
- name: Restart Jellyfin
3+
ansible.builtin.systemd_service:
4+
name: jellyfin
5+
scope: user
6+
daemon_reload: true
7+
state: restarted
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
- name: Ensure media directory exists
3+
ansible.builtin.file:
4+
path: "{{ jellyfin_media_path }}"
5+
state: directory
6+
mode: "0755"
7+
8+
- name: Ensure quadlet directory exists
9+
ansible.builtin.file:
10+
path: "{{ ansible_facts['env']['HOME'] }}/.config/containers/systemd"
11+
state: directory
12+
mode: "0755"
13+
14+
- name: Deploy Jellyfin quadlet container file
15+
ansible.builtin.template:
16+
src: jellyfin.container.j2
17+
dest: "{{ ansible_facts['env']['HOME'] }}/.config/containers/systemd/jellyfin.container"
18+
mode: "0644"
19+
notify: Restart Jellyfin
20+
21+
- name: Enable loginctl linger
22+
ansible.builtin.command:
23+
cmd: loginctl enable-linger {{ ansible_facts['env']['USER'] }}
24+
changed_when: false
25+
when: jellyfin_enable_linger
26+
27+
- name: Reload systemd user daemon
28+
ansible.builtin.systemd_service:
29+
daemon_reload: true
30+
scope: user
31+
32+
- name: Enable and start Jellyfin service
33+
ansible.builtin.systemd_service:
34+
name: jellyfin
35+
scope: user
36+
enabled: true
37+
state: started
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# {{ ansible_managed }}
2+
3+
[Container]
4+
Image={{ jellyfin_image }}
5+
{% if jellyfin_auto_update %}
6+
AutoUpdate=registry
7+
{% endif %}
8+
PublishPort={{ jellyfin_web_port }}:8096
9+
{% if jellyfin_https_port is defined %}
10+
PublishPort={{ jellyfin_https_port }}:8920
11+
{% endif %}
12+
{% if jellyfin_discovery_port_dlna is defined %}
13+
PublishPort={{ jellyfin_discovery_port_dlna }}:1900/udp
14+
{% endif %}
15+
{% if jellyfin_discovery_port_client is defined %}
16+
PublishPort={{ jellyfin_discovery_port_client }}:7359/udp
17+
{% endif %}
18+
Volume={{ jellyfin_config_volume }}:/config:Z
19+
Volume={{ jellyfin_cache_volume }}:/cache:Z
20+
Volume={{ jellyfin_media_path }}:/media:{% if jellyfin_media_readonly %}ro,{% endif %}Z
21+
22+
[Service]
23+
Restart=always
24+
25+
[Install]
26+
WantedBy=default.target

0 commit comments

Comments
 (0)