From a73fb2aa297527303e665b83803b46ee25dd8702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:53:43 +0200 Subject: [PATCH] feat(arr): add prowlarr, radarr and seerr roles --- roles/prowlarr/README.md | 44 ++++++++ roles/prowlarr/defaults/main.yml | 21 ++++ roles/prowlarr/handlers/main.yml | 20 ++++ roles/prowlarr/meta/main.yml | 2 + roles/prowlarr/tasks/main.yml | 104 ++++++++++++++++++ roles/prowlarr/templates/nginx-vhost.conf.j2 | 58 ++++++++++ roles/prowlarr/templates/prowlarr.service.j2 | 15 +++ roles/prowlarr/templates/prowlarr.yaml.j2 | 45 ++++++++ roles/radarr/README.md | 48 +++++++++ roles/radarr/defaults/main.yml | 27 +++++ roles/radarr/handlers/main.yml | 20 ++++ roles/radarr/meta/main.yml | 2 + roles/radarr/tasks/main.yml | 107 +++++++++++++++++++ roles/radarr/templates/nginx-vhost.conf.j2 | 58 ++++++++++ roles/radarr/templates/radarr.service.j2 | 15 +++ roles/radarr/templates/radarr.yaml.j2 | 57 ++++++++++ roles/seerr/README.md | 50 +++++++++ roles/seerr/defaults/main.yml | 28 +++++ roles/seerr/handlers/main.yml | 20 ++++ roles/seerr/meta/main.yml | 2 + roles/seerr/tasks/main.yml | 104 ++++++++++++++++++ roles/seerr/templates/nginx-vhost.conf.j2 | 58 ++++++++++ roles/seerr/templates/seerr.service.j2 | 15 +++ roles/seerr/templates/seerr.yaml.j2 | 50 +++++++++ 24 files changed, 970 insertions(+) create mode 100644 roles/prowlarr/README.md create mode 100644 roles/prowlarr/defaults/main.yml create mode 100644 roles/prowlarr/handlers/main.yml create mode 100644 roles/prowlarr/meta/main.yml create mode 100644 roles/prowlarr/tasks/main.yml create mode 100644 roles/prowlarr/templates/nginx-vhost.conf.j2 create mode 100644 roles/prowlarr/templates/prowlarr.service.j2 create mode 100644 roles/prowlarr/templates/prowlarr.yaml.j2 create mode 100644 roles/radarr/README.md create mode 100644 roles/radarr/defaults/main.yml create mode 100644 roles/radarr/handlers/main.yml create mode 100644 roles/radarr/meta/main.yml create mode 100644 roles/radarr/tasks/main.yml create mode 100644 roles/radarr/templates/nginx-vhost.conf.j2 create mode 100644 roles/radarr/templates/radarr.service.j2 create mode 100644 roles/radarr/templates/radarr.yaml.j2 create mode 100644 roles/seerr/README.md create mode 100644 roles/seerr/defaults/main.yml create mode 100644 roles/seerr/handlers/main.yml create mode 100644 roles/seerr/meta/main.yml create mode 100644 roles/seerr/tasks/main.yml create mode 100644 roles/seerr/templates/nginx-vhost.conf.j2 create mode 100644 roles/seerr/templates/seerr.service.j2 create mode 100644 roles/seerr/templates/seerr.yaml.j2 diff --git a/roles/prowlarr/README.md b/roles/prowlarr/README.md new file mode 100644 index 0000000..355aea9 --- /dev/null +++ b/roles/prowlarr/README.md @@ -0,0 +1,44 @@ +# Prowlarr Role + +Deploys [Prowlarr](https://prowlarr.com/) - an indexer manager/proxy for the *arr stack - using Podman with k8s files. The `linuxserver/prowlarr` image bundles Prowlarr and its SQLite database. + +## Prerequisites + +The `podman` role must have been applied to the host first (shared service, intentionally not declared in `meta/main.yml` to keep prowlarr deploys fast). Order it before `prowlarr` in the playbook for fresh installs. + +## Role Variables + +See `defaults/main.yml` for all available variables and their default values. + +## Storage + +- `prowlarr_config_dir` - SQLite database, indexer definitions and API keys. Backed up. + +## Ports + +| Port | Protocol | Purpose | +| ------------------------ | -------- | ------- | +| `prowlarr_port` (9696) | tcp | Web UI | + +## Nginx Reverse Proxy + +Set `prowlarr_nginx_enabled: true` and `prowlarr_nginx_hostname` to expose the web UI over HTTPS. Certificates are provisioned via the shared `nginx` role's certbot tasks. If the hostname is not the apex, point a DNS record at the host first. + +## Backup + +The role exposes `prowlarr_backup_paths` (config only). Add a job in the host inventory to include it: + +```yaml +backup_jobs: + - name: prowlarr + repositories: [next_s3] + paths: "{{ prowlarr_backup_paths }}" +``` + +## Troubleshooting + +```sh +systemctl --user status prowlarr +journalctl --user -u prowlarr -f +podman logs prowlarr +``` diff --git a/roles/prowlarr/defaults/main.yml b/roles/prowlarr/defaults/main.yml new file mode 100644 index 0000000..5078017 --- /dev/null +++ b/roles/prowlarr/defaults/main.yml @@ -0,0 +1,21 @@ +--- +# Prowlarr container image and version +prowlarr_image: lscr.io/linuxserver/prowlarr +prowlarr_version: latest + +# Network +prowlarr_port: 9696 + +# Timezone +prowlarr_timezone: UTC + +# Config holds the SQLite database (indexers, API keys, settings) +prowlarr_config_dir: "{{ podman_projects_dir }}/prowlarr/config" + +# Backup manifest - consumed by roles/backup via inventory `backup_jobs` +prowlarr_backup_paths: + - "{{ prowlarr_config_dir }}" + +# Nginx reverse proxy configuration +prowlarr_nginx_enabled: false +prowlarr_nginx_hostname: prowlarr.nas.local diff --git a/roles/prowlarr/handlers/main.yml b/roles/prowlarr/handlers/main.yml new file mode 100644 index 0000000..48819a5 --- /dev/null +++ b/roles/prowlarr/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Reload systemd user + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Restart Prowlarr + ansible.builtin.systemd: + name: prowlarr.service + state: restarted + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/prowlarr/meta/main.yml b/roles/prowlarr/meta/main.yml new file mode 100644 index 0000000..23d65c7 --- /dev/null +++ b/roles/prowlarr/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/roles/prowlarr/tasks/main.yml b/roles/prowlarr/tasks/main.yml new file mode 100644 index 0000000..a218d36 --- /dev/null +++ b/roles/prowlarr/tasks/main.yml @@ -0,0 +1,104 @@ +--- +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ ansible_facts['getent_passwd'][ansible_user][4] }}" + +- name: Create Prowlarr project directory + ansible.builtin.file: + path: "{{ podman_projects_dir | default('/opt/podman') }}/prowlarr" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create Prowlarr state directory + ansible.builtin.file: + path: "{{ prowlarr_config_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0750" + +- name: Get current Prowlarr image ID + ansible.builtin.command: "podman image inspect --format {% raw %}{{.Id}}{% endraw %} {{ prowlarr_image }}:{{ prowlarr_version }}" + register: prowlarr_image_before + failed_when: false # image may not exist yet on first deploy + changed_when: false + become: false + become_user: "{{ ansible_user }}" + +- name: Pull Prowlarr container image + ansible.builtin.command: "podman pull --quiet {{ prowlarr_image }}:{{ prowlarr_version }}" + register: prowlarr_pull + changed_when: prowlarr_pull.stdout != prowlarr_image_before.stdout + become: false + become_user: "{{ ansible_user }}" + notify: Restart Prowlarr + +- name: Deploy Kubernetes YAML for Prowlarr + ansible.builtin.template: + src: prowlarr.yaml.j2 + dest: "{{ podman_projects_dir | default('/opt/podman') }}/prowlarr/prowlarr.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Restart Prowlarr + +- name: Create systemd user directory for Prowlarr + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create systemd service for Prowlarr (user scope) + ansible.builtin.template: + src: prowlarr.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/prowlarr.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user + +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + changed_when: false + when: ansible_user != 'root' + +- name: Enable and start Prowlarr service (user scope) + ansible.builtin.systemd: + name: prowlarr.service + enabled: true + state: started + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Provision TLS certificate for Prowlarr + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ prowlarr_nginx_hostname }}" + when: prowlarr_nginx_enabled + +- name: Deploy nginx vhost configuration for Prowlarr + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/prowlarr.conf" + owner: root + group: root + mode: "0644" + when: prowlarr_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost configuration for Prowlarr + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/prowlarr.conf" + state: absent + when: not prowlarr_nginx_enabled + notify: Reload nginx diff --git a/roles/prowlarr/templates/nginx-vhost.conf.j2 b/roles/prowlarr/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..e6962df --- /dev/null +++ b/roles/prowlarr/templates/nginx-vhost.conf.j2 @@ -0,0 +1,58 @@ +# Prowlarr vhost with Let's Encrypt (Certbot) +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ prowlarr_nginx_hostname }}; + + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ prowlarr_nginx_hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ prowlarr_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ prowlarr_nginx_hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols | default('TLSv1.3') }}; + ssl_prefer_server_ciphers on; + +{% if nginx_log_backend | default('journald') == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_prowlarr; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_prowlarr; +{% else %} + access_log /var/log/nginx/{{ prowlarr_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ prowlarr_nginx_hostname }}_error.log; +{% endif %} + + client_max_body_size 20M; + + location / { + proxy_pass http://127.0.0.1:{{ prowlarr_port }}; + proxy_set_header Host $http_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; + + # WebSocket support for real-time UI updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + proxy_buffering off; + proxy_read_timeout 3600s; + } +} diff --git a/roles/prowlarr/templates/prowlarr.service.j2 b/roles/prowlarr/templates/prowlarr.service.j2 new file mode 100644 index 0000000..36d093e --- /dev/null +++ b/roles/prowlarr/templates/prowlarr.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Prowlarr Indexer Manager + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/prowlarr +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} prowlarr.yaml +ExecStop=/usr/bin/podman kube down prowlarr.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/prowlarr/templates/prowlarr.yaml.j2 b/roles/prowlarr/templates/prowlarr.yaml.j2 new file mode 100644 index 0000000..4f0568c --- /dev/null +++ b/roles/prowlarr/templates/prowlarr.yaml.j2 @@ -0,0 +1,45 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: prowlarr + labels: + app: prowlarr +spec: + containers: + - name: prowlarr + image: {{ prowlarr_image }}:{{ prowlarr_version }} + ports: + - containerPort: {{ prowlarr_port }} + hostPort: {{ prowlarr_port }} + env: + - name: PUID + value: "{{ ansible_facts['getent_passwd'][ansible_user][1] }}" + - name: PGID + value: "{{ ansible_facts['getent_passwd'][ansible_user][2] }}" + - name: TZ + value: "{{ prowlarr_timezone }}" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: config + mountPath: /config + livenessProbe: + tcpSocket: + port: {{ prowlarr_port }} + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: config + hostPath: + path: {{ prowlarr_config_dir }} + type: Directory diff --git a/roles/radarr/README.md b/roles/radarr/README.md new file mode 100644 index 0000000..a7e8d77 --- /dev/null +++ b/roles/radarr/README.md @@ -0,0 +1,48 @@ +# Radarr Role + +Deploys [Radarr](https://radarr.video/) - a movie collection manager for Usenet and BitTorrent - using Podman with k8s files. The `linuxserver/radarr` image bundles Radarr and its SQLite database. + +## Prerequisites + +The `podman` role must have been applied to the host first. Order it before `radarr` in the playbook for fresh installs. + +## Role Variables + +See `defaults/main.yml` for all available variables and their default values. + +## Storage + +- `radarr_config_dir` - SQLite database, library and API keys. Backed up. +- `radarr_movies_dir` - movie library, mounted read-write at `/movies`. Use `/movies` as the root folder in the Radarr UI. +- `radarr_downloads_dir` - completed downloads, shared with Deluge. Mounted read-write at `/downloads`; point the download client there. + +`radarr_movies_dir` lives on ZFS (`/mnt/andromeda`), `radarr_downloads_dir` under `{{ podman_projects_dir }}` - cross-filesystem imports are copies, not hardlinks. + +## Ports + +| Port | Protocol | Purpose | +| ---------------------- | -------- | ------- | +| `radarr_port` (7878) | tcp | Web UI | + +## Nginx Reverse Proxy + +Set `radarr_nginx_enabled: true` and `radarr_nginx_hostname` to expose the web UI over HTTPS. Certificates are provisioned via the shared `nginx` role's certbot tasks. If the hostname is not the apex, point a DNS record at the host first. + +## Backup + +The role exposes `radarr_backup_paths` (config only). Add a job in the host inventory to include it: + +```yaml +backup_jobs: + - name: radarr + repositories: [next_s3] + paths: "{{ radarr_backup_paths }}" +``` + +## Troubleshooting + +```sh +systemctl --user status radarr +journalctl --user -u radarr -f +podman logs radarr +``` diff --git a/roles/radarr/defaults/main.yml b/roles/radarr/defaults/main.yml new file mode 100644 index 0000000..b553b27 --- /dev/null +++ b/roles/radarr/defaults/main.yml @@ -0,0 +1,27 @@ +--- +# Radarr container image and version +radarr_image: lscr.io/linuxserver/radarr +radarr_version: latest + +# Network +radarr_port: 7878 + +# Timezone +radarr_timezone: UTC + +# Config holds the SQLite database (library, indexers, download clients) +radarr_config_dir: "{{ podman_projects_dir }}/radarr/config" + +# Movie library (root folder in the Radarr UI -> /movies) +radarr_movies_dir: /mnt/andromeda/movies + +# Completed downloads, shared with the download client (-> /downloads) +radarr_downloads_dir: "{{ podman_projects_dir }}/deluge/downloads" + +# Backup manifest - consumed by roles/backup via inventory `backup_jobs` +radarr_backup_paths: + - "{{ radarr_config_dir }}" + +# Nginx reverse proxy configuration +radarr_nginx_enabled: false +radarr_nginx_hostname: radarr.nas.local diff --git a/roles/radarr/handlers/main.yml b/roles/radarr/handlers/main.yml new file mode 100644 index 0000000..3756b65 --- /dev/null +++ b/roles/radarr/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Reload systemd user + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Restart Radarr + ansible.builtin.systemd: + name: radarr.service + state: restarted + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/radarr/meta/main.yml b/roles/radarr/meta/main.yml new file mode 100644 index 0000000..23d65c7 --- /dev/null +++ b/roles/radarr/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/roles/radarr/tasks/main.yml b/roles/radarr/tasks/main.yml new file mode 100644 index 0000000..90749b5 --- /dev/null +++ b/roles/radarr/tasks/main.yml @@ -0,0 +1,107 @@ +--- +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ ansible_facts['getent_passwd'][ansible_user][4] }}" + +- name: Create Radarr project directory + ansible.builtin.file: + path: "{{ podman_projects_dir | default('/opt/podman') }}/radarr" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create Radarr state directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0750" + loop: + - "{{ radarr_config_dir }}" + - "{{ radarr_downloads_dir }}" + +- name: Get current Radarr image ID + ansible.builtin.command: "podman image inspect --format {% raw %}{{.Id}}{% endraw %} {{ radarr_image }}:{{ radarr_version }}" + register: radarr_image_before + failed_when: false # image may not exist yet on first deploy + changed_when: false + become: false + become_user: "{{ ansible_user }}" + +- name: Pull Radarr container image + ansible.builtin.command: "podman pull --quiet {{ radarr_image }}:{{ radarr_version }}" + register: radarr_pull + changed_when: radarr_pull.stdout != radarr_image_before.stdout + become: false + become_user: "{{ ansible_user }}" + notify: Restart Radarr + +- name: Deploy Kubernetes YAML for Radarr + ansible.builtin.template: + src: radarr.yaml.j2 + dest: "{{ podman_projects_dir | default('/opt/podman') }}/radarr/radarr.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Restart Radarr + +- name: Create systemd user directory for Radarr + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create systemd service for Radarr (user scope) + ansible.builtin.template: + src: radarr.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/radarr.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user + +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + changed_when: false + when: ansible_user != 'root' + +- name: Enable and start Radarr service (user scope) + ansible.builtin.systemd: + name: radarr.service + enabled: true + state: started + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Provision TLS certificate for Radarr + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ radarr_nginx_hostname }}" + when: radarr_nginx_enabled + +- name: Deploy nginx vhost configuration for Radarr + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/radarr.conf" + owner: root + group: root + mode: "0644" + when: radarr_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost configuration for Radarr + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/radarr.conf" + state: absent + when: not radarr_nginx_enabled + notify: Reload nginx diff --git a/roles/radarr/templates/nginx-vhost.conf.j2 b/roles/radarr/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..445b0c0 --- /dev/null +++ b/roles/radarr/templates/nginx-vhost.conf.j2 @@ -0,0 +1,58 @@ +# Radarr vhost with Let's Encrypt (Certbot) +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ radarr_nginx_hostname }}; + + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ radarr_nginx_hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ radarr_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ radarr_nginx_hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols | default('TLSv1.3') }}; + ssl_prefer_server_ciphers on; + +{% if nginx_log_backend | default('journald') == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_radarr; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_radarr; +{% else %} + access_log /var/log/nginx/{{ radarr_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ radarr_nginx_hostname }}_error.log; +{% endif %} + + client_max_body_size 20M; + + location / { + proxy_pass http://127.0.0.1:{{ radarr_port }}; + proxy_set_header Host $http_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; + + # WebSocket support for real-time UI updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + proxy_buffering off; + proxy_read_timeout 3600s; + } +} diff --git a/roles/radarr/templates/radarr.service.j2 b/roles/radarr/templates/radarr.service.j2 new file mode 100644 index 0000000..45fbe04 --- /dev/null +++ b/roles/radarr/templates/radarr.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Radarr Movie Manager + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/radarr +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} radarr.yaml +ExecStop=/usr/bin/podman kube down radarr.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/radarr/templates/radarr.yaml.j2 b/roles/radarr/templates/radarr.yaml.j2 new file mode 100644 index 0000000..3bccbfd --- /dev/null +++ b/roles/radarr/templates/radarr.yaml.j2 @@ -0,0 +1,57 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: radarr + labels: + app: radarr +spec: + containers: + - name: radarr + image: {{ radarr_image }}:{{ radarr_version }} + ports: + - containerPort: {{ radarr_port }} + hostPort: {{ radarr_port }} + env: + - name: PUID + value: "{{ ansible_facts['getent_passwd'][ansible_user][1] }}" + - name: PGID + value: "{{ ansible_facts['getent_passwd'][ansible_user][2] }}" + - name: TZ + value: "{{ radarr_timezone }}" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: config + mountPath: /config + - name: movies + mountPath: /movies + - name: downloads + mountPath: /downloads + livenessProbe: + tcpSocket: + port: {{ radarr_port }} + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: config + hostPath: + path: {{ radarr_config_dir }} + type: Directory + - name: movies + hostPath: + path: {{ radarr_movies_dir }} + type: Directory + - name: downloads + hostPath: + path: {{ radarr_downloads_dir }} + type: Directory diff --git a/roles/seerr/README.md b/roles/seerr/README.md new file mode 100644 index 0000000..b8b72f1 --- /dev/null +++ b/roles/seerr/README.md @@ -0,0 +1,50 @@ +# Seerr Role + +Deploys [Seerr](https://seerr.dev/) - the media request manager (successor of Overseerr/Jellyseerr) - using Podman with k8s files. Uses the official `ghcr.io/seerr-team/seerr` image. + +## Prerequisites + +The `podman` role must have been applied to the host first. Order it before `seerr` in the playbook for fresh installs. + +## Role Variables + +See `defaults/main.yml` for all available variables and their default values. + +## First Login + +The web UI listens on `http://:{{ seerr_port }}`. Complete the setup wizard, then point Seerr at Jellyfin and the *arr services. Because Seerr runs in a container, reach the host-side services via the pasta gateway (`{{ podman_gw_gateway }}`, default `100.64.0.1`) or `host.containers.internal`, not `localhost`. + +## Storage + +- `seerr_config_dir` - SQLite database, users, requests and settings. Backed up. + +The image runs as the unprivileged `node` user (UID/GID 1000). The pod maps the rootless pod user onto that UID via the `io.podman.annotations.userns` annotation, so the host-owned config directory is writable. + +## Ports + +| Port | Protocol | Purpose | +| --------------------- | -------- | ------- | +| `seerr_port` (5055) | tcp | Web UI | + +## Nginx Reverse Proxy + +Set `seerr_nginx_enabled: true` and `seerr_nginx_hostname` to expose the web UI over HTTPS. Certificates are provisioned via the shared `nginx` role's certbot tasks. If the hostname is not the apex, point a DNS record at the host first. + +## Backup + +The role exposes `seerr_backup_paths` (config only). Add a job in the host inventory to include it: + +```yaml +backup_jobs: + - name: seerr + repositories: [next_s3] + paths: "{{ seerr_backup_paths }}" +``` + +## Troubleshooting + +```sh +systemctl --user status seerr +journalctl --user -u seerr -f +podman logs seerr +``` diff --git a/roles/seerr/defaults/main.yml b/roles/seerr/defaults/main.yml new file mode 100644 index 0000000..6cfdba3 --- /dev/null +++ b/roles/seerr/defaults/main.yml @@ -0,0 +1,28 @@ +--- +# Seerr container image and version +seerr_image: ghcr.io/seerr-team/seerr +seerr_version: latest + +# Network +seerr_port: 5055 + +# Timezone and log level (error/warn/info/debug) +seerr_timezone: UTC +seerr_log_level: info + +# Config holds the SQLite database (users, requests, settings) +seerr_config_dir: "{{ podman_projects_dir }}/seerr/config" + +# The image runs as the unprivileged `node` user (UID/GID 1000). Rootless +# podman maps the pod user onto that UID so the container can write the +# host-owned config directory. +seerr_container_uid: 1000 +seerr_container_gid: 1000 + +# Backup manifest - consumed by roles/backup via inventory `backup_jobs` +seerr_backup_paths: + - "{{ seerr_config_dir }}" + +# Nginx reverse proxy configuration +seerr_nginx_enabled: false +seerr_nginx_hostname: seerr.nas.local diff --git a/roles/seerr/handlers/main.yml b/roles/seerr/handlers/main.yml new file mode 100644 index 0000000..538d7d6 --- /dev/null +++ b/roles/seerr/handlers/main.yml @@ -0,0 +1,20 @@ +--- +- name: Reload systemd user + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Restart Seerr + ansible.builtin.systemd: + name: seerr.service + state: restarted + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/seerr/meta/main.yml b/roles/seerr/meta/main.yml new file mode 100644 index 0000000..23d65c7 --- /dev/null +++ b/roles/seerr/meta/main.yml @@ -0,0 +1,2 @@ +--- +dependencies: [] diff --git a/roles/seerr/tasks/main.yml b/roles/seerr/tasks/main.yml new file mode 100644 index 0000000..66045b9 --- /dev/null +++ b/roles/seerr/tasks/main.yml @@ -0,0 +1,104 @@ +--- +- name: Get home directory for {{ ansible_user }} + ansible.builtin.getent: + database: passwd + key: "{{ ansible_user }}" + +- name: Set user home directory fact + ansible.builtin.set_fact: + user_home_dir: "{{ ansible_facts['getent_passwd'][ansible_user][4] }}" + +- name: Create Seerr project directory + ansible.builtin.file: + path: "{{ podman_projects_dir | default('/opt/podman') }}/seerr" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create Seerr state directory + ansible.builtin.file: + path: "{{ seerr_config_dir }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0750" + +- name: Get current Seerr image ID + ansible.builtin.command: "podman image inspect --format {% raw %}{{.Id}}{% endraw %} {{ seerr_image }}:{{ seerr_version }}" + register: seerr_image_before + failed_when: false # image may not exist yet on first deploy + changed_when: false + become: false + become_user: "{{ ansible_user }}" + +- name: Pull Seerr container image + ansible.builtin.command: "podman pull --quiet {{ seerr_image }}:{{ seerr_version }}" + register: seerr_pull + changed_when: seerr_pull.stdout != seerr_image_before.stdout + become: false + become_user: "{{ ansible_user }}" + notify: Restart Seerr + +- name: Deploy Kubernetes YAML for Seerr + ansible.builtin.template: + src: seerr.yaml.j2 + dest: "{{ podman_projects_dir | default('/opt/podman') }}/seerr/seerr.yaml" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Restart Seerr + +- name: Create systemd user directory for Seerr + ansible.builtin.file: + path: "{{ user_home_dir }}/.config/systemd/user" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: Create systemd service for Seerr (user scope) + ansible.builtin.template: + src: seerr.service.j2 + dest: "{{ user_home_dir }}/.config/systemd/user/seerr.service" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0644" + notify: Reload systemd user + +- name: Enable lingering for user {{ ansible_user }} + ansible.builtin.command: "loginctl enable-linger {{ ansible_user }}" + changed_when: false + when: ansible_user != 'root' + +- name: Enable and start Seerr service (user scope) + ansible.builtin.systemd: + name: seerr.service + enabled: true + state: started + scope: user + become: false + become_user: "{{ ansible_user }}" + +- name: Provision TLS certificate for Seerr + ansible.builtin.include_tasks: "{{ role_path }}/../nginx/tasks/certbot.yml" + vars: + certbot_hostname: "{{ seerr_nginx_hostname }}" + when: seerr_nginx_enabled + +- name: Deploy nginx vhost configuration for Seerr + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/seerr.conf" + owner: root + group: root + mode: "0644" + when: seerr_nginx_enabled + notify: Reload nginx + +- name: Remove nginx vhost configuration for Seerr + ansible.builtin.file: + path: "{{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/seerr.conf" + state: absent + when: not seerr_nginx_enabled + notify: Reload nginx diff --git a/roles/seerr/templates/nginx-vhost.conf.j2 b/roles/seerr/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..c79fc98 --- /dev/null +++ b/roles/seerr/templates/nginx-vhost.conf.j2 @@ -0,0 +1,58 @@ +# Seerr vhost with Let's Encrypt (Certbot) +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ seerr_nginx_hostname }}; + + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ seerr_nginx_hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ seerr_nginx_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ seerr_nginx_hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols | default('TLSv1.3') }}; + ssl_prefer_server_ciphers on; + +{% if nginx_log_backend | default('journald') == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_seerr; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_seerr; +{% else %} + access_log /var/log/nginx/{{ seerr_nginx_hostname }}_access.log main; + error_log /var/log/nginx/{{ seerr_nginx_hostname }}_error.log; +{% endif %} + + client_max_body_size 20M; + + location / { + proxy_pass http://127.0.0.1:{{ seerr_port }}; + proxy_set_header Host $http_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; + + # WebSocket support for real-time UI updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + proxy_buffering off; + proxy_read_timeout 3600s; + } +} diff --git a/roles/seerr/templates/seerr.service.j2 b/roles/seerr/templates/seerr.service.j2 new file mode 100644 index 0000000..de419ad --- /dev/null +++ b/roles/seerr/templates/seerr.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Seerr Media Request Manager + +[Service] +Type=notify +NotifyAccess=all +WorkingDirectory={{ podman_projects_dir | default('/opt/podman') }}/seerr +ExecStart=/usr/bin/podman kube play --replace --service-container=true --network=pasta:--map-host-loopback={{ podman_gw_gateway }} seerr.yaml +ExecStop=/usr/bin/podman kube down seerr.yaml +Restart=on-failure +RestartSec=10 +TimeoutStartSec=180 + +[Install] +WantedBy=default.target diff --git a/roles/seerr/templates/seerr.yaml.j2 b/roles/seerr/templates/seerr.yaml.j2 new file mode 100644 index 0000000..ada22d9 --- /dev/null +++ b/roles/seerr/templates/seerr.yaml.j2 @@ -0,0 +1,50 @@ +--- +apiVersion: v1 +kind: Pod +metadata: + name: seerr + labels: + app: seerr + annotations: + # Map the rootless pod user onto the image's `node` UID/GID so it can + # write the host-owned config directory. + io.podman.annotations.userns: "keep-id:uid={{ seerr_container_uid }},gid={{ seerr_container_gid }}" +spec: + containers: + - name: seerr + image: {{ seerr_image }}:{{ seerr_version }} + ports: + - containerPort: {{ seerr_port }} + hostPort: {{ seerr_port }} + env: + - name: TZ + value: "{{ seerr_timezone }}" + - name: LOG_LEVEL + value: "{{ seerr_log_level }}" + - name: PORT + value: "{{ seerr_port }}" + volumeMounts: + - name: localtime + mountPath: /etc/localtime + readOnly: true + - name: config + mountPath: /app/config + livenessProbe: + httpGet: + path: /api/v1/settings/public + port: {{ seerr_port }} + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + restartPolicy: Never + + volumes: + - name: localtime + hostPath: + path: /etc/localtime + type: File + - name: config + hostPath: + path: {{ seerr_config_dir }} + type: Directory