From dbac5ede16a4a88465b08765dccfa85c73cd5c08 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Thu, 8 Oct 2026 10:23:17 +0200 Subject: [PATCH] Add a role for Jitsi in Docker Compose We need this role to follow the recommended deployment approach from https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/ This role enables us to keep our config and customizations separate from releases of the Compose setup the Jitsi project publishs. --- playbooks/deploy.yaml | 7 + roles/jitsi/README.md | 57 ++++++ roles/jitsi/defaults/main.yaml | 8 + roles/jitsi/handlers/main.yaml | 19 ++ roles/jitsi/meta/argument_specs.yaml | 57 ++++++ roles/jitsi/meta/main.yaml | 4 + roles/jitsi/tasks/main.yaml | 186 ++++++++++++++++++ .../jitsi/templates/compose.override.yaml.j2 | 40 ++++ roles/jitsi/templates/jitsi.env.j2 | 46 +++++ roles/jitsi/templates/turnserver.conf.j2 | 34 ++++ roles/jitsi/vars/main.yaml | 4 + 11 files changed, 462 insertions(+) create mode 100644 roles/jitsi/README.md create mode 100644 roles/jitsi/defaults/main.yaml create mode 100644 roles/jitsi/handlers/main.yaml create mode 100644 roles/jitsi/meta/argument_specs.yaml create mode 100644 roles/jitsi/meta/main.yaml create mode 100644 roles/jitsi/tasks/main.yaml create mode 100644 roles/jitsi/templates/compose.override.yaml.j2 create mode 100644 roles/jitsi/templates/jitsi.env.j2 create mode 100644 roles/jitsi/templates/turnserver.conf.j2 create mode 100644 roles/jitsi/vars/main.yaml diff --git a/playbooks/deploy.yaml b/playbooks/deploy.yaml index 2a1498ac..fd86de91 100644 --- a/playbooks/deploy.yaml +++ b/playbooks/deploy.yaml @@ -110,6 +110,13 @@ tags: - docker_compose +- name: Ensure Jitsi deployment on jitsi_hosts + hosts: jitsi_hosts + roles: + - jitsi + tags: + - jitsi + - name: Ensure NGINX deployment on nginx_hosts hosts: nginx_hosts:!public_reverse_proxy_hosts roles: diff --git a/roles/jitsi/README.md b/roles/jitsi/README.md new file mode 100644 index 00000000..bd3db9bb --- /dev/null +++ b/roles/jitsi/README.md @@ -0,0 +1,57 @@ +# Role `jitsi` + +A role for deploying [Jitsi Meet](https://jitsi.org/) using the official [docker-jitsi-meet](https://github.com/jitsi/docker-jitsi-meet) release. + +The given release is downloaded and unpacked unmodified to `/opt/jitsi/releases/` and `/opt/jitsi/release` is pointed to it. +The `.env` file at `/opt/jitsi/.env` is fully managed by this role; `env.example` and `gen-passwords.sh` of the release are not used. +The `.env` also sets `COMPOSE_PROJECT_NAME` and `COMPOSE_FILE`, so `docker compose` can be run by hand in `/opt/jitsi`. +The Jitsi configuration directory (`CONFIG`) is `/opt/jitsi/config`, with persistent state in `/opt/jitsi/config/storage/`. +The image version (`JITSI_IMAGE_VERSION`) always equals the release version, so upgrading means bumping `jitsi__version`. +Releases other than the configured one are removed. + +TLS certificates are obtained by the web container itself using Let's Encrypt, so ports 80/tcp and 443/tcp as well as 10000/udp (media) must be reachable. + +Optionally, a [coturn](https://github.com/coturn/coturn) TURN server and the [Prometheus Jitsi Meet exporter](https://github.com/systemli/prometheus-jitsi-meet-exporter) are deployed as additional services of the same Compose project, defined in `/opt/jitsi/compose.override.yaml`. +coturn uses the certificate obtained by the web container and is restarted weekly to pick up renewals. +It needs 3478/udp+tcp, 5349/tcp and the relay port range 49152-65535/udp to be reachable. +The exporter scrapes the JVB statistics and listens on port 9888. + +## Supported Distributions + +Should work on Debian-based distributions. + +## Required Arguments + +- `jitsi__version`: The docker-jitsi-meet release to deploy, e.g. `stable-11248`. +- `jitsi__domain`: The domain Jitsi is served on. Used for `PUBLIC_URL` and the Let's Encrypt certificate. +- `jitsi__letsencrypt_email`: The e-mail address for the Let's Encrypt account. +- `jitsi__jicofo_auth_password`: XMPP password for Jicofo. Generate using `openssl rand -hex 16`. +- `jitsi__jvb_auth_password`: XMPP password for the JVB. Generate using `openssl rand -hex 16`. + +## Optional Arguments + +- `jitsi__timezone`: The time zone for the containers. + Defaults to `Europe/Berlin`. +- `jitsi__jvb_advertise_ips`: List of IP addresses the JVB should advertise for media (`JVB_ADVERTISE_IPS`). + Defaults to `[ ]`. +- `jitsi__whiteboard`: Whether to deploy the Excalidraw whiteboard backend (`whiteboard.yml`). + Defaults to `false`. +- `jitsi__turn`: Whether to deploy coturn and configure Jitsi to use it. + Defaults to `false`. +- `jitsi__turn_secret`: The shared secret between Prosody and coturn. Required if `jitsi__turn` is `true`. + Generate using `openssl rand -hex 32`. +- `jitsi__prometheus_exporter`: Whether to deploy the Prometheus Jitsi Meet exporter on port 9888. + Defaults to `false`. +- `jitsi__env`: Dictionary of additional variables to put into the `.env` file. + See `env.example` of the release and the handbook for available variables. + Defaults to `{ }`. +- `jitsi__custom_config_js`: Content of `custom-config.js`, which gets appended to the generated `config.js`. + Defaults to `""` (no file). +- `jitsi__custom_interface_config_js`: Content of `custom-interface_config.js`, which gets appended to the generated `interface_config.js`. + Defaults to `""` (no file). + +## Links & Resources + +- +- +- diff --git a/roles/jitsi/defaults/main.yaml b/roles/jitsi/defaults/main.yaml new file mode 100644 index 00000000..9782b636 --- /dev/null +++ b/roles/jitsi/defaults/main.yaml @@ -0,0 +1,8 @@ +jitsi__timezone: Europe/Berlin +jitsi__jvb_advertise_ips: [ ] +jitsi__whiteboard: false +jitsi__turn: false +jitsi__prometheus_exporter: false +jitsi__env: { } +jitsi__custom_config_js: "" +jitsi__custom_interface_config_js: "" diff --git a/roles/jitsi/handlers/main.yaml b/roles/jitsi/handlers/main.yaml new file mode 100644 index 00000000..7bde7f02 --- /dev/null +++ b/roles/jitsi/handlers/main.yaml @@ -0,0 +1,19 @@ +- name: restart jitsi web + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + services: + - web + state: restarted + become: true + +- name: restart jitsi coturn + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + services: + - coturn + state: restarted + become: true diff --git a/roles/jitsi/meta/argument_specs.yaml b/roles/jitsi/meta/argument_specs.yaml new file mode 100644 index 00000000..2c65ca54 --- /dev/null +++ b/roles/jitsi/meta/argument_specs.yaml @@ -0,0 +1,57 @@ +argument_specs: + main: + options: + jitsi__version: + type: str + required: true + jitsi__domain: + type: str + required: true + jitsi__letsencrypt_email: + type: str + required: true + jitsi__jicofo_auth_password: + type: str + required: true + no_log: true + jitsi__jvb_auth_password: + type: str + required: true + no_log: true + jitsi__timezone: + type: str + required: false + default: Europe/Berlin + jitsi__jvb_advertise_ips: + type: list + elements: str + required: false + default: [ ] + jitsi__whiteboard: + type: bool + required: false + default: false + jitsi__turn: + type: bool + required: false + default: false + jitsi__turn_secret: + type: str + required: false + no_log: true + jitsi__prometheus_exporter: + type: bool + required: false + default: false + jitsi__env: + type: dict + required: false + default: { } + jitsi__custom_config_js: + type: str + required: false + default: "" + jitsi__custom_interface_config_js: + type: str + required: false + default: "" diff --git a/roles/jitsi/meta/main.yaml b/roles/jitsi/meta/main.yaml new file mode 100644 index 00000000..1fbb2364 --- /dev/null +++ b/roles/jitsi/meta/main.yaml @@ -0,0 +1,4 @@ +--- +allow_duplicates: false +dependencies: + - role: docker diff --git a/roles/jitsi/tasks/main.yaml b/roles/jitsi/tasks/main.yaml new file mode 100644 index 00000000..a7beaaa4 --- /dev/null +++ b/roles/jitsi/tasks/main.yaml @@ -0,0 +1,186 @@ +- name: make sure a TURN secret is given, if TURN is enabled + ansible.builtin.assert: + that: + - jitsi__turn_secret is defined + - jitsi__turn_secret | length > 0 + quiet: true + when: jitsi__turn + +- name: make sure the Jitsi base directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + owner: root + group: root + become: true + loop: + - "{{ jitsi__base_dir }}" + - "{{ jitsi__base_dir }}/releases" + +- name: make sure the docker-jitsi-meet release is present + ansible.builtin.unarchive: + src: "https://github.com/jitsi/docker-jitsi-meet/archive/refs/tags/{{ jitsi__version }}.tar.gz" + remote_src: true + dest: "{{ jitsi__base_dir }}/releases" + creates: "{{ jitsi__release_dir }}/docker-compose.yml" + owner: root + group: root + become: true + +- name: point the `release` symlink to the configured release + ansible.builtin.file: + src: "{{ jitsi__release_dir }}" + dest: "{{ jitsi__base_dir }}/release" + state: link + owner: root + group: root + become: true + +# Configuration read by the containers at startup; managed by Ansible. +- name: make sure the configuration input directories exist + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/{{ item }}" + state: directory + mode: "0755" + owner: root + group: root + become: true + loop: + - "" + - web + - prosody + - prosody/config + - prosody/prosody-plugins-custom + - jicofo + - jvb + - storage + - tmp + +# State written by the containers, which run as uid/gid 1000. +# The certificate directory is created here, as Docker would otherwise create it owned by +# root for the coturn bind mount, before the web container gets to write the certificate. +- name: make sure the storage and tmp directories exist and are writable by the containers + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/{{ item }}" + state: directory + mode: "0750" + owner: "1000" + group: "1000" + become: true + loop: + - storage/prosody + - storage/web + - storage/web/acme-certs + - "storage/web/acme-certs/{{ jitsi__domain }}" + - storage/transcripts + - tmp/web-crontabs + - tmp/web-load-test + +- name: deploy custom web configuration + ansible.builtin.copy: + content: "{{ item.content }}" + dest: "{{ jitsi__config_dir }}/web/{{ item.name }}" + mode: "0644" + owner: root + group: root + become: true + when: item.content != "" + loop: + - name: custom-config.js + content: "{{ jitsi__custom_config_js }}" + - name: custom-interface_config.js + content: "{{ jitsi__custom_interface_config_js }}" + loop_control: + label: "{{ item.name }}" + notify: restart jitsi web + +- name: remove unconfigured custom web configuration + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/web/{{ item.name }}" + state: absent + become: true + when: item.content == "" + loop: + - name: custom-config.js + content: "{{ jitsi__custom_config_js }}" + - name: custom-interface_config.js + content: "{{ jitsi__custom_interface_config_js }}" + loop_control: + label: "{{ item.name }}" + notify: restart jitsi web + +- name: make sure the coturn directory exists + ansible.builtin.file: + path: "{{ jitsi__base_dir }}/coturn" + state: "{{ 'directory' if jitsi__turn else 'absent' }}" + mode: "0755" + owner: root + group: root + become: true + +# Readable by uid 1000, which coturn runs as. +- name: deploy the coturn configuration + ansible.builtin.template: + src: turnserver.conf.j2 + dest: "{{ jitsi__base_dir }}/coturn/turnserver.conf" + mode: "0640" + owner: root + group: "1000" + become: true + when: jitsi__turn + notify: restart jitsi coturn + +# coturn doesn't notice the certificate renewed by the web container on its own. +- name: ensure the weekly coturn restart cron job is present + ansible.builtin.cron: + name: "jitsi coturn restart for certificate renewal" + special_time: weekly + job: "cd {{ jitsi__base_dir }} && docker compose restart coturn >/dev/null" + state: "{{ 'present' if jitsi__turn else 'absent' }}" + become: true + +# Changes to these make `docker compose up` below recreate the affected containers. +- name: deploy the compose override file + ansible.builtin.template: + src: compose.override.yaml.j2 + dest: "{{ jitsi__base_dir }}/compose.override.yaml" + mode: "0644" + owner: root + group: root + become: true + +- name: deploy the .env file + ansible.builtin.template: + src: jitsi.env.j2 + dest: "{{ jitsi__base_dir }}/.env" + mode: "0600" + owner: root + group: root + become: true + +- name: docker compose up + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + state: present + pull: missing + remove_orphans: true + become: true + +- name: find old releases + ansible.builtin.find: + paths: "{{ jitsi__base_dir }}/releases" + file_type: directory + excludes: "docker-jitsi-meet-{{ jitsi__version }}" + register: jitsi__old_releases + +- name: remove old releases + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + become: true + loop: "{{ jitsi__old_releases.files }}" + loop_control: + label: "{{ item.path }}" diff --git a/roles/jitsi/templates/compose.override.yaml.j2 b/roles/jitsi/templates/compose.override.yaml.j2 new file mode 100644 index 00000000..05842569 --- /dev/null +++ b/roles/jitsi/templates/compose.override.yaml.j2 @@ -0,0 +1,40 @@ +# {{ ansible_managed }} +# Services in addition to the ones of the docker-jitsi-meet release. + +# Docker Compose explicitly disables IPv6 for networks not enabling it, overriding the +# daemon's default-network-opts. +networks: + meet.jitsi: + enable_ipv6: true + +services: +{% if jitsi__turn %} + coturn: + image: docker.io/coturn/coturn:4.18.0 + restart: unless-stopped + # Recommended by coturn, as Docker performs badly with large port ranges. + network_mode: host + # Needs to be able to read the certificate obtained by the web container. + user: "1000:1000" + command: ["-c", "/etc/coturn/turnserver.conf"] + volumes: + - {{ jitsi__base_dir }}/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro + - {{ jitsi__config_dir }}/storage/web/acme-certs/{{ jitsi__domain }}:/certs:ro + tmpfs: + - /var/lib/coturn +{% endif %} +{% if jitsi__prometheus_exporter %} + prometheus-jitsi-meet-exporter: + image: docker.io/systemli/prometheus-jitsi-meet-exporter:1.4.2 + restart: unless-stopped + command: ["-videobridge-url", "http://jvb:8080/colibri/stats"] + ports: + - "9888:9888" + depends_on: + - jvb + networks: + meet.jitsi: +{% endif %} +{% if not (jitsi__turn or jitsi__prometheus_exporter) %} + {} +{% endif %} diff --git a/roles/jitsi/templates/jitsi.env.j2 b/roles/jitsi/templates/jitsi.env.j2 new file mode 100644 index 00000000..f09b0414 --- /dev/null +++ b/roles/jitsi/templates/jitsi.env.j2 @@ -0,0 +1,46 @@ +# {{ ansible_managed }} +# Only the settings differing from the defaults are set here. +# Reference: {{ jitsi__base_dir }}/release/env.example and +# https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker + +# Allow running `docker compose` by hand in {{ jitsi__base_dir }}. +COMPOSE_PROJECT_NAME=jitsi +COMPOSE_FILE={{ jitsi__compose_files | join(':') }} + +CONFIG={{ jitsi__config_dir }} +JITSI_IMAGE_VERSION={{ jitsi__version }} +TZ={{ jitsi__timezone }} +PUBLIC_URL=https://{{ jitsi__domain }} + +HTTP_PORT=80 +HTTPS_PORT=443 +ENABLE_HTTP_REDIRECT=1 +ENABLE_LETSENCRYPT=1 +LETSENCRYPT_DOMAIN={{ jitsi__domain }} +LETSENCRYPT_EMAIL={{ jitsi__letsencrypt_email }} +LETSENCRYPT_ACME_SERVER=letsencrypt +{% if jitsi__jvb_advertise_ips | length > 0 %} +JVB_ADVERTISE_IPS={{ jitsi__jvb_advertise_ips | join(',') }} +{% endif %} +{% if jitsi__whiteboard %} + +WHITEBOARD_COLLAB_SERVER_URL_BASE=http://whiteboard.meet.jitsi +{% endif %} +{% if jitsi__turn %} + +TURN_CREDENTIALS={{ jitsi__turn_secret }} +TURN_HOST={{ jitsi__domain }} +TURN_PORT=3478 +TURN_TRANSPORT=udp,tcp +TURNS_HOST={{ jitsi__domain }} +TURNS_PORT=5349 +{% endif %} +{% if jitsi__env | length > 0 %} + +{% for key, value in jitsi__env.items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} + +JICOFO_AUTH_PASSWORD={{ jitsi__jicofo_auth_password }} +JVB_AUTH_PASSWORD={{ jitsi__jvb_auth_password }} diff --git a/roles/jitsi/templates/turnserver.conf.j2 b/roles/jitsi/templates/turnserver.conf.j2 new file mode 100644 index 00000000..28770f81 --- /dev/null +++ b/roles/jitsi/templates/turnserver.conf.j2 @@ -0,0 +1,34 @@ +# {{ ansible_managed }} +listening-port=3478 +tls-listening-port=5349 +realm={{ jitsi__domain }} +use-auth-secret +static-auth-secret={{ jitsi__turn_secret }} +cert=/certs/fullchain.pem +pkey=/certs/key.pem +no-tlsv1 +no-tlsv1_1 +no-cli +no-multicast-peers +no-loopback-peers +log-file=stdout +# Don't relay to private and otherwise special addresses. +denied-peer-ip=0.0.0.0-0.255.255.255 +denied-peer-ip=10.0.0.0-10.255.255.255 +denied-peer-ip=100.64.0.0-100.127.255.255 +denied-peer-ip=127.0.0.0-127.255.255.255 +denied-peer-ip=169.254.0.0-169.254.255.255 +denied-peer-ip=172.16.0.0-172.31.255.255 +denied-peer-ip=192.0.0.0-192.0.0.255 +denied-peer-ip=192.0.2.0-192.0.2.255 +denied-peer-ip=192.88.99.0-192.88.99.255 +denied-peer-ip=192.168.0.0-192.168.255.255 +denied-peer-ip=198.18.0.0-198.19.255.255 +denied-peer-ip=198.51.100.0-198.51.100.255 +denied-peer-ip=203.0.113.0-203.0.113.255 +denied-peer-ip=240.0.0.0-255.255.255.255 +denied-peer-ip=::1 +denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff +denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 +denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff +denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff diff --git a/roles/jitsi/vars/main.yaml b/roles/jitsi/vars/main.yaml new file mode 100644 index 00000000..21fe76b6 --- /dev/null +++ b/roles/jitsi/vars/main.yaml @@ -0,0 +1,4 @@ +jitsi__base_dir: /opt/jitsi +jitsi__config_dir: "{{ jitsi__base_dir }}/config" +jitsi__release_dir: "{{ jitsi__base_dir }}/releases/docker-jitsi-meet-{{ jitsi__version }}" +jitsi__compose_files: "{{ ['release/docker-compose.yml'] + (['release/whiteboard.yml'] if jitsi__whiteboard else []) + ['compose.override.yaml'] }}"