Manage Jitsi through Ansible using custom role #172

Open
stb wants to merge 5 commits from jitsi-docker-compose-role into main
11 changed files with 462 additions and 0 deletions
Showing only changes of commit dbac5ede16 - Show all commits

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.
Stefan Bethke 2026-10-08 10:23:17 +02:00

View file

@ -110,6 +110,13 @@
tags: tags:
- docker_compose - docker_compose
- name: Ensure Jitsi deployment on jitsi_hosts
hosts: jitsi_hosts
roles:
- jitsi
tags:
- jitsi
- name: Ensure NGINX deployment on nginx_hosts - name: Ensure NGINX deployment on nginx_hosts
hosts: nginx_hosts:!public_reverse_proxy_hosts hosts: nginx_hosts:!public_reverse_proxy_hosts
roles: roles:

57
roles/jitsi/README.md Normal file
View file

@ -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
- <https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/>
- <https://github.com/jitsi/docker-jitsi-meet>
- <https://github.com/jitsi/docker-jitsi-meet/releases>

View file

@ -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: ""

View file

@ -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

View file

@ -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: ""

View file

@ -0,0 +1,4 @@
---
allow_duplicates: false
dependencies:
- role: docker

186
roles/jitsi/tasks/main.yaml Normal file
View file

@ -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 }}"

View file

@ -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 %}

View file

@ -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 }}

View file

@ -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

View file

@ -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'] }}"