diff --git a/roles/matrix-bot-go-neb/defaults/main.yml b/roles/matrix-bot-go-neb/defaults/main.yml
new file mode 100644
index 000000000..4dd4f1f66
--- /dev/null
+++ b/roles/matrix-bot-go-neb/defaults/main.yml
@@ -0,0 +1,231 @@
+# Go-NEB is a Matrix bot written in Go. It is the successor to Matrix-NEB, the original Matrix bot written in Python.
+# See: https://github.com/matrix-org/go-neb
+
+matrix_bot_go_neb_enabled: true
+matrix_bot_go_neb_version: latest
+matrix_bot_go_neb_docker_image: "matrixdotorg/go-neb:{{ matrix_bot_go_neb_version }}"
+matrix_bot_go_neb_docker_image_force_pull: "{{ matrix_bot_go_neb_docker_image.endswith(':latest') }}"
+
+matrix_bot_go_neb_base_path: "{{ matrix_base_data_path }}/go-neb"
+matrix_bot_go_neb_config_path: "{{ matrix_bot_go_neb_base_path }}/config"
+matrix_bot_go_neb_config_path_in_container: "/config/config.yaml"
+matrix_bot_go_neb_data_path: "{{ matrix_bot_go_neb_base_path }}/data"
+matrix_bot_go_neb_data_store_path: "{{ matrix_bot_go_neb_data_path }}/store"
+
+# Controls whether the matrix-bot-go-neb container exposes its HTTP port (tcp/4050 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:4050"), or empty string to not expose.
+matrix_bot_go_neb_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_bot_go_neb_container_extra_arguments: []
+
+# List of systemd services that matrix-bot-go-neb.service depends on
+matrix_bot_go_neb_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-bot-go-neb.service wants
+matrix_bot_go_neb_systemd_wanted_services_list: []
+
+# Database-related configuration fields.
+#
+# MUST be "sqlite3". No other type is supported.
+matrix_bot_go_neb_database_engine: 'sqlite3'
+
+matrix_bot_go_neb_sqlite_database_path_local: "{{ matrix_bot_go_neb_data_path }}/bot.db"
+matrix_bot_go_neb_sqlite_database_path_in_container: "/data/bot.db"
+
+matrix_bot_go_neb_storage_database: "{{
+	{
+		'sqlite3': (matrix_bot_go_neb_sqlite_database_path_in_container + '?_busy_timeout=5000'),
+	}[matrix_bot_go_neb_database_engine]
+}}"
+
+# The bot's username(s). These users need to be created manually beforehand.
+# The access tokens that the bot uses to authenticate.
+# Generate one as described in
+# https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-dimension.md#access-token
+# via curl. With the element method, you might run into decryption problems (see https://github.com/matrix-org/go-neb#quick-start)
+matrix_bot_go_neb_clients: []
+#  - UserID: "@goneb:{{ matrix_domain }}"
+#    AccessToken: "MDASDASJDIASDJASDAFGFRGER"
+#    DeviceID: "DEVICE1"
+#    HomeserverURL: "{{ matrix_homeserver_container_url }}"
+#    Sync: true
+#    AutoJoinRooms: true
+#    DisplayName: "Go-NEB!"
+#    AcceptVerificationFromUsers: [":{{ matrix_domain }}"]
+#
+#  - UserID: "@another_goneb:{{ matrix_domain }}"
+#    AccessToken: "MDASDASJDIASDJASDAFGFRGER"
+#    DeviceID: "DEVICE2"
+#    HomeserverURL: "{{ matrix_homeserver_container_url }}"
+#    Sync: false
+#    AutoJoinRooms: false
+#    DisplayName: "Go-NEB!"
+#    AcceptVerificationFromUsers: ["^@admin:{{ matrix_domain }}"]
+
+# The list of realms which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# See the docs for /configureAuthRealm for the full list of options:
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#ConfigureAuthRealmRequest
+matrix_bot_go_neb_realms: []
+#  - ID: "github_realm"
+#    Type: "github"
+#    Config: {} # No need for client ID or Secret as Go-NEB isn't generating OAuth URLs
+
+# The list of *authenticated* sessions which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# The full list of options are shown below: there is no single HTTP endpoint
+# which maps to this section.
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#Session
+matrix_bot_go_neb_sessions: []
+#  - SessionID: "your_github_session"
+#    RealmID: "github_realm"
+#    UserID: "@YOUR_USER_ID:{{ matrix_domain }}" # This needs to be the username of the person that's allowed to use the !github commands
+#    Config:
+#      # Populate these fields by generating a "Personal Access Token" on github.com
+#      AccessToken: "YOUR_GITHUB_ACCESS_TOKEN"
+#      Scopes: "admin:org_hook,admin:repo_hook,repo,user"
+
+# The list of services which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# See the docs for /configureService for the full list of options:
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#ConfigureServiceRequest
+matrix_bot_go_neb_services: []
+#  - ID: "echo_service"
+#    Type: "echo"
+#    UserID: "@goneb:{{ matrix_domain }}"
+#    Config: {}
+
+## Can be obtained from https://developers.giphy.com/dashboard/
+#  - ID: "giphy_service"
+#    Type: "giphy"
+#    UserID: "@goneb:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#      api_key: "qwg4672vsuyfsfe"
+#      use_downsized: false
+#
+## This service has been dead for over a year :/
+#  - ID: "guggy_service"
+#    Type: "guggy"
+#    UserID: "@goneb:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#      api_key: "2356saaqfhgfe"
+#
+## API Key via https://developers.google.com/custom-search/v1/introduction
+## CX via http://www.google.com/cse/manage/all
+## https://stackoverflow.com/questions/6562125/getting-a-cx-id-for-custom-search-google-api-python
+## 'Search the entire web' and 'Image search' enabled for best results
+#  - ID: "google_service"
+#    Type: "google"
+#    UserID: "@goneb:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#      api_key: "AIzaSyA4FD39m9"
+#      cx: "AIASDFWSRRtrtr"
+#
+## Get a key via https://api.imgur.com/oauth2/addclient
+## Select "oauth2 without callback url"
+#  - ID: "imgur_service"
+#    Type: "imgur"
+#    UserID: "@imgur:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#      client_id: "AIzaSyA4FD39m9"
+#      client_secret: "somesecret"
+#
+#  - ID: "wikipedia_service"
+#    Type: "wikipedia"
+#    UserID: "@goneb:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#
+#  - ID: "rss_service"
+#    Type: "rssbot"
+#    UserID: "@another_goneb:{{ matrix_domain }}"
+#    Config:
+#      feeds:
+#        "http://lorem-rss.herokuapp.com/feed?unit=second&interval=60":
+#          rooms: ["!qmElAGdFYCHoCJuaNt:localhost"]
+#          must_include:
+#            author:
+#              - author1
+#            description:
+#              - lorem
+#              - ipsum
+#          must_not_include:
+#            title:
+#              - Lorem
+#              - Ipsum
+#
+#  - ID: "github_cmd_service"
+#    Type: "github"
+#    UserID: "@goneb:{{ matrix_domain }}" # requires a Syncing client
+#    Config:
+#      RealmID: "github_realm"
+#
+#    # Make sure your BASE_URL can be accessed by Github!
+#  - ID: "github_webhook_service"
+#    Type: "github-webhook"
+#    UserID: "@another_goneb:{{ matrix_domain }}"
+#    Config:
+#      RealmID: "github_realm"
+#      ClientUserID: "@YOUR_USER_ID:{{ matrix_domain }}" # needs to be an authenticated user so Go-NEB can create webhooks. Check the UserID field in the github_realm in matrix_bot_go_neb_sessions.
+#      Rooms:
+#        "!someroom:id":
+#          Repos:
+#            "matrix-org/synapse":
+#              Events: ["push", "issues"]
+#            "matrix-org/dendron":
+#              Events: ["pull_request"]
+#        "!anotherroom:id":
+#          Repos:
+#            "matrix-org/synapse":
+#              Events: ["push", "issues"]
+#            "matrix-org/dendron":
+#              Events: ["pull_request"]
+#
+#  - ID: "slackapi_service"
+#    Type: "slackapi"
+#    UserID: "@slackapi:{{ matrix_domain }}"
+#    Config:
+#      Hooks:
+#        "hook1":
+#          RoomID: "!someroom:id"
+#          MessageType: "m.text" # default is m.text
+#
+#  - ID: "alertmanager_service"
+#    Type: "alertmanager"
+#    UserID: "@alertmanager:{{ matrix_domain }}"
+#    Config:
+#      # This is for information purposes only. It should point to Go-NEB path as follows:
+#      # `/services/hooks/<base64 encoded service ID>`
+#      # Where in this case "service ID" is "alertmanager_service"
+#      # Make sure your BASE_URL can be accessed by the Alertmanager instance!
+#      webhook_url: "http://localhost/services/hooks/YWxlcnRtYW5hZ2VyX3NlcnZpY2U"
+#      # Each room will get the notification with the alert rendered with the given template
+#      rooms:
+#        "!someroomid:domain.tld":
+#          text_template: "{{range .Alerts -}} [{{ .Status }}] {{index .Labels \"alertname\" }}: {{index .Annotations \"description\"}} {{ end -}}"
+#          html_template: "{{range .Alerts -}}  {{ $severity := index .Labels \"severity\" }}    {{ if eq .Status \"firing\" }}      {{ if eq $severity \"critical\"}}        <font color='red'><b>[FIRING - CRITICAL]</b></font>      {{ else if eq $severity \"warning\"}}        <font color='orange'><b>[FIRING - WARNING]</b></font>      {{ else }}        <b>[FIRING - {{ $severity }}]</b>      {{ end }}    {{ else }}      <font color='green'><b>[RESOLVED]</b></font>    {{ end }}  {{ index .Labels \"alertname\"}} : {{ index .Annotations \"description\"}}   <a href=\"{{ .GeneratorURL }}\">source</a><br/>{{end -}}"
+#          msg_type: "m.text"  # Must be either `m.text` or `m.notice`
+
+# Default configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_bot_go_neb_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_bot_go_neb_configuration_yaml: "{{ lookup('template', 'templates/config.yaml.j2') }}"
+
+matrix_bot_go_neb_configuration_extension_yaml: |
+  # Your custom YAML configuration goes here.
+  # This configuration extends the default starting configuration (`matrix_bot_go_neb_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_bot_go_neb_configuration_yaml`.
+
+matrix_bot_go_neb_configuration_extension: "{{ matrix_bot_go_neb_configuration_extension_yaml|from_yaml if matrix_bot_go_neb_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_bot_go_neb_configuration_yaml`.
+matrix_bot_go_neb_configuration: "{{ matrix_bot_go_neb_configuration_yaml|from_yaml|combine(matrix_bot_go_neb_configuration_extension, recursive=True) }}"
+
diff --git a/roles/matrix-bot-go-neb/tasks/init.yml b/roles/matrix-bot-go-neb/tasks/init.yml
new file mode 100644
index 000000000..169f5978a
--- /dev/null
+++ b/roles/matrix-bot-go-neb/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-bot-go-neb.service'] }}"
+  when: matrix_bot_go_neb_enabled|bool
diff --git a/roles/matrix-bot-go-neb/tasks/main.yml b/roles/matrix-bot-go-neb/tasks/main.yml
new file mode 100644
index 000000000..1a4fe70a5
--- /dev/null
+++ b/roles/matrix-bot-go-neb/tasks/main.yml
@@ -0,0 +1,21 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_bot_go_neb_enabled|bool"
+  tags:
+    - setup-all
+    - setup-bot-go-neb
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: "run_setup|bool and matrix_bot_go_neb_enabled|bool"
+  tags:
+    - setup-all
+    - setup-bot-go-neb
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: "run_setup|bool and not matrix_bot_go_neb_enabled|bool"
+  tags:
+    - setup-all
+    - setup-bot-go-neb
diff --git a/roles/matrix-bot-go-neb/tasks/setup_install.yml b/roles/matrix-bot-go-neb/tasks/setup_install.yml
new file mode 100644
index 000000000..e26be0802
--- /dev/null
+++ b/roles/matrix-bot-go-neb/tasks/setup_install.yml
@@ -0,0 +1,50 @@
+---
+
+- set_fact:
+    matrix_bot_go_neb_requires_restart: false
+
+- name: Ensure go-neb paths exist
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_bot_go_neb_config_path }}", when: true }
+    - { path: "{{ matrix_bot_go_neb_data_path }}", when: true }
+    - { path: "{{ matrix_bot_go_neb_data_store_path }}", when: true }
+  when: "item.when|bool"
+
+- name: Ensure go-neb image is pulled
+  docker_image:
+    name: "{{ matrix_bot_go_neb_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_bot_go_neb_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_bot_go_neb_docker_image_force_pull }}"
+
+- name: Ensure go-neb config installed
+  copy:
+    content: "{{ matrix_bot_go_neb_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_bot_go_neb_config_path }}/config.yaml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-bot-go-neb.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-bot-go-neb.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-bot-go-neb.service"
+    mode: 0644
+  register: matrix_bot_go_neb_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-bot-go-neb.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_bot_go_neb_systemd_service_result.changed|bool"
+
+- name: Ensure matrix-bot-go-neb.service restarted, if necessary
+  service:
+    name: "matrix-bot-go-neb.service"
+    state: restarted
+  when: "matrix_bot_go_neb_requires_restart|bool"
diff --git a/roles/matrix-bot-go-neb/tasks/setup_uninstall.yml b/roles/matrix-bot-go-neb/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..49ad1fe75
--- /dev/null
+++ b/roles/matrix-bot-go-neb/tasks/setup_uninstall.yml
@@ -0,0 +1,35 @@
+---
+
+- name: Check existence of matrix-go-neb service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-bot-go-neb.service"
+  register: matrix_bot_go_neb_service_stat
+
+- name: Ensure matrix-go-neb is stopped
+  service:
+    name: matrix-bot-go-neb
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_bot_go_neb_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-bot-go-neb.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-bot-go-neb.service"
+    state: absent
+  when: "matrix_bot_go_neb_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-bot-go-neb.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_bot_go_neb_service_stat.stat.exists|bool"
+
+- name: Ensure Matrix go-neb paths don't exist
+  file:
+    path: "{{ matrix_bot_go_neb_base_path }}"
+    state: absent
+
+- name: Ensure go-neb Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_bot_go_neb_docker_image }}"
+    state: absent
diff --git a/roles/matrix-bot-go-neb/tasks/validate_config.yml b/roles/matrix-bot-go-neb/tasks/validate_config.yml
new file mode 100644
index 000000000..7b292250a
--- /dev/null
+++ b/roles/matrix-bot-go-neb/tasks/validate_config.yml
@@ -0,0 +1,13 @@
+---
+
+- name: Fail if there's not at least 1 client
+  fail:
+    msg: >-
+      You need at least 1 client in the matrix_bot_go_neb_clients block.
+  when: matrix_bot_go_neb_clients is not defined or matrix_bot_go_neb_clients[0] is not defined
+
+- name: Fail if there's not at least 1 service
+  fail:
+    msg: >-
+      You need at least 1 service in the matrix_bot_go_neb_services block.
+  when: matrix_bot_go_neb_services is not defined or matrix_bot_go_neb_services[0] is not defined
diff --git a/roles/matrix-bot-go-neb/templates/config.yaml.j2 b/roles/matrix-bot-go-neb/templates/config.yaml.j2
new file mode 100644
index 000000000..c72dbf8df
--- /dev/null
+++ b/roles/matrix-bot-go-neb/templates/config.yaml.j2
@@ -0,0 +1,44 @@
+# Go-NEB Configuration File
+#
+# This file provides an alternative way to configure Go-NEB which does not involve HTTP APIs.
+#
+# This file can be supplied to go-neb by the environment variable `CONFIG_FILE=config.yaml`.
+# It will force Go-NEB to operate in "config" mode. This means:
+#   - Go-NEB will ONLY use the data contained inside this file.
+#   - All of Go-NEB's /admin HTTP listeners will be disabled. You will be unable to add new services at runtime.
+#   - The environment variable `DATABASE_URL` will be ignored and an in-memory database will be used instead.
+#
+# This file is broken down into 4 sections which matches the following HTTP APIs:
+#   - /configureClient
+#   - /configureAuthRealm
+#   - /configureService
+#   - /requestAuthSession (redirects not supported)
+
+# The list of clients which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# See the docs for /configureClient for the full list of options:
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#ClientConfig
+clients:
+  {{ matrix_bot_go_neb_clients|to_json }}
+
+# The list of realms which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# See the docs for /configureAuthRealm for the full list of options:
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#ConfigureAuthRealmRequest
+realms:
+  {{ matrix_bot_go_neb_realms|to_json }}
+
+# The list of *authenticated* sessions which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# The full list of options are shown below: there is no single HTTP endpoint
+# which maps to this section.
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#Session
+sessions:
+  {{ matrix_bot_go_neb_sessions|to_json }}
+
+# The list of services which Go-NEB is aware of.
+# Delete or modify this list as appropriate.
+# See the docs for /configureService for the full list of options:
+# https://matrix-org.github.io/go-neb/pkg/github.com/matrix-org/go-neb/api/index.html#ConfigureServiceRequest
+services:
+  {{ matrix_bot_go_neb_services|to_json }}
diff --git a/roles/matrix-bot-go-neb/templates/systemd/matrix-bot-go-neb.service.j2 b/roles/matrix-bot-go-neb/templates/systemd/matrix-bot-go-neb.service.j2
new file mode 100644
index 000000000..eabf11372
--- /dev/null
+++ b/roles/matrix-bot-go-neb/templates/systemd/matrix-bot-go-neb.service.j2
@@ -0,0 +1,49 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Go-NEB bot
+{% for service in matrix_bot_go_neb_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_bot_go_neb_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-bot-go-neb 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-bot-go-neb 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-bot-go-neb \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_bot_go_neb_container_http_host_bind_port %}
+			-p {{ matrix_bot_go_neb_container_http_host_bind_port }}:4050 \
+			{% endif %}
+			-e 'BIND_ADDRESS=:4050' \
+			-e 'DATABASE_TYPE={{ matrix_bot_go_neb_database_engine }}' \
+			-e 'BASE_URL=https://{{ matrix_server_fqn_bot_go_neb }}' \
+			-e 'CONFIG_FILE={{ matrix_bot_go_neb_config_path_in_container }}' \
+			-e 'DATABASE_URL={{ matrix_bot_go_neb_storage_database }}' \
+			--mount type=bind,src={{ matrix_bot_go_neb_config_path }},dst=/config,ro \
+			--mount type=bind,src={{ matrix_bot_go_neb_data_path }},dst=/data \
+			--entrypoint=/bin/sh \
+			{% for arg in matrix_bot_go_neb_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_bot_go_neb_docker_image }} \
+			-c "go-neb /config/config.yaml"
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-bot-go-neb 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-bot-go-neb 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-bot-go-neb
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-bridge-heisenbridge/defaults/main.yml b/roles/matrix-bridge-heisenbridge/defaults/main.yml
new file mode 100644
index 000000000..be95af8da
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/defaults/main.yml
@@ -0,0 +1,47 @@
+# heisenbridge is a bouncer-style Matrix IRC bridge
+# See: https://github.com/hifi/heisenbridge
+
+matrix_heisenbridge_enabled: true
+
+matrix_heisenbridge_version: latest
+matrix_heisenbridge_docker_image: "{{ matrix_container_global_registry_prefix }}hif1/heisenbridge:{{ matrix_heisenbridge_version }}"
+matrix_heisenbridge_docker_image_force_pull: "{{ matrix_heisenbridge_docker_image.endswith(':latest') }}"
+
+# Set this to your Matrix ID if you want to enforce the owner, otherwise first _local_ user becomes one
+matrix_heisenbridge_owner: ""
+
+# Enabling identd will bind to host port 113/TCP
+matrix_heisenbridge_identd_enabled: false
+
+matrix_heisenbridge_base_path: "{{ matrix_base_data_path }}/heisenbridge"
+
+# A list of extra arguments to pass to the container
+matrix_heisenbridge_container_extra_arguments: []
+
+# List of systemd services that service depends on.
+matrix_heisenbridge_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that service wants
+matrix_heisenbridge_systemd_wanted_services_list: []
+
+matrix_heisenbridge_homeserver_url: "{{ matrix_homeserver_container_url }}"
+
+matrix_heisenbridge_appservice_token: ''
+matrix_heisenbridge_homeserver_token: ''
+
+# Default registration file
+matrix_heisenbridge_registration_yaml:
+  id: heisenbridge
+  url: http://matrix-heisenbridge:9898
+  as_token: "{{ matrix_heisenbridge_appservice_token }}"
+  hs_token: "{{ matrix_heisenbridge_homeserver_token }}" 
+  rate_limited: false
+  sender_localpart: heisenbridge
+  namespaces:
+    users:
+    - regex: '@hbirc_.*'
+      exclusive: true
+    aliases: []
+    rooms: []
+
+matrix_heisenbridge_registration: "{{ matrix_heisenbridge_registration_yaml|from_yaml }}"
diff --git a/roles/matrix-bridge-heisenbridge/tasks/init.yml b/roles/matrix-bridge-heisenbridge/tasks/init.yml
new file mode 100644
index 000000000..18e89b681
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/tasks/init.yml
@@ -0,0 +1,24 @@
+# If the matrix-synapse role is not used, `matrix_synapse_role_executed` won't exist.
+# We don't want to fail in such cases.
+- name: Fail if matrix-synapse role already executed
+  fail:
+    msg: >-
+      The matrix-bridge-heisenbridge role needs to execute before the matrix-synapse role.
+  when: "matrix_heisenbridge_enabled and matrix_synapse_role_executed|default(False)"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-heisenbridge.service'] }}"
+  when: matrix_heisenbridge_enabled|bool
+
+# If the matrix-synapse role is not used, these variables may not exist.
+- set_fact:
+    matrix_synapse_container_extra_arguments: >
+      {{ matrix_synapse_container_extra_arguments|default([]) }}
+      +
+      ["--mount type=bind,src={{ matrix_heisenbridge_base_path }}/registration.yaml,dst=/heisenbridge-registration.yaml,ro"]
+
+    matrix_synapse_app_service_config_files: >
+      {{ matrix_synapse_app_service_config_files|default([]) }}
+      +
+      {{ ["/heisenbridge-registration.yaml"] }}
+  when: matrix_heisenbridge_enabled|bool
diff --git a/roles/matrix-bridge-heisenbridge/tasks/main.yml b/roles/matrix-bridge-heisenbridge/tasks/main.yml
new file mode 100644
index 000000000..1358709d8
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/tasks/main.yml
@@ -0,0 +1,15 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: "run_setup|bool and matrix_heisenbridge_enabled|bool"
+  tags:
+    - setup-all
+    - setup-heisenbridge
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: "run_setup|bool and not matrix_heisenbridge_enabled|bool"
+  tags:
+    - setup-all
+    - setup-heisenbridge
diff --git a/roles/matrix-bridge-heisenbridge/tasks/setup_install.yml b/roles/matrix-bridge-heisenbridge/tasks/setup_install.yml
new file mode 100644
index 000000000..03cf9ec3e
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/tasks/setup_install.yml
@@ -0,0 +1,38 @@
+---
+
+- name: Ensure heisenbridge image is pulled
+  docker_image:
+    name: "{{ matrix_heisenbridge_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_heisenbridge_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}" 
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_heisenbridge_docker_image_force_pull }}" 
+
+- name: Ensure heisenbridge paths exist
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_heisenbridge_base_path }}"
+
+- name: Ensure heisenbridge registration.yaml installed if provided
+  copy:
+    content: "{{ matrix_heisenbridge_registration|to_nice_yaml }}"
+    dest: "{{ matrix_heisenbridge_base_path }}/registration.yaml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-heisenbridge.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-heisenbridge.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-heisenbridge.service"
+    mode: 0644
+  register: matrix_heisenbridge_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-heisenbridge.service installation
+  service:
+    daemon_reload: yes
+  when: matrix_heisenbridge_systemd_service_result.changed
diff --git a/roles/matrix-bridge-heisenbridge/tasks/setup_uninstall.yml b/roles/matrix-bridge-heisenbridge/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..853faf7a2
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/tasks/setup_uninstall.yml
@@ -0,0 +1,24 @@
+---
+
+- name: Check existence of matrix-heisenbridge service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-heisenbridge.service"
+  register: matrix_heisenbridge_service_stat
+
+- name: Ensure matrix-heisenbridge is stopped
+  service:
+    name: matrix-heisenbridge
+    state: stopped
+    daemon_reload: yes
+  when: "matrix_heisenbridge_service_stat.stat.exists"
+
+- name: Ensure matrix-heisenbridge.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-heisenbridge.service"
+    state: absent
+  when: "matrix_heisenbridge_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-heisenbridge.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_heisenbridge_service_stat.stat.exists"
diff --git a/roles/matrix-bridge-heisenbridge/templates/systemd/matrix-heisenbridge.service.j2 b/roles/matrix-bridge-heisenbridge/templates/systemd/matrix-heisenbridge.service.j2
new file mode 100644
index 000000000..e27b88f1d
--- /dev/null
+++ b/roles/matrix-bridge-heisenbridge/templates/systemd/matrix-heisenbridge.service.j2
@@ -0,0 +1,51 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=a bouncer-style Matrix IRC bridge
+{% for service in matrix_heisenbridge_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_heisenbridge_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_docker }} kill matrix-heisenbridge
+ExecStartPre=-{{ matrix_host_command_docker }} rm matrix-heisenbridge
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-heisenbridge \
+          --log-driver=none \
+          --user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+          --cap-drop=ALL \
+          --network={{ matrix_docker_network }} \
+          {% if matrix_heisenbridge_identd_enabled %}
+          -p 113:13113 \
+          {% endif %}
+          -v {{ matrix_heisenbridge_base_path }}:/config:z \
+          {% for arg in matrix_heisenbridge_container_extra_arguments %}
+          {{ arg }} \
+          {% endfor %}
+          {{ matrix_heisenbridge_docker_image }} \
+          {% if matrix_heisenbridge_identd_enabled %}
+          --identd \
+          --identd-port 13113 \
+          {% endif %}
+          {% if matrix_heisenbridge_owner %}
+          -o {{ matrix_heisenbridge_owner }} \
+          {% endif %}
+          --config /config/registration.yaml \
+          --listen-address 0.0.0.0 \
+          --listen-port 9898 \
+          {{ matrix_heisenbridge_homeserver_url }}
+
+ExecStop=-{{ matrix_host_command_docker }} kill matrix-heisenbridge
+ExecStop=-{{ matrix_host_command_docker }} rm matrix-heisenbridge
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-heisenbridge
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-client-hydrogen/defaults/main.yml b/roles/matrix-client-hydrogen/defaults/main.yml
new file mode 100644
index 000000000..fa2e38fd7
--- /dev/null
+++ b/roles/matrix-client-hydrogen/defaults/main.yml
@@ -0,0 +1,68 @@
+matrix_client_hydrogen_enabled: true
+
+# Self building is used by default because the `config.json` file is only read at build time.
+# The pre-built images also were not functional as of 2021-05-15.
+matrix_client_hydrogen_container_image_self_build: true
+matrix_client_hydrogen_container_image_self_build_repo: "https://github.com/vector-im/hydrogen-web.git"
+
+matrix_client_hydrogen_version: v0.2.0
+matrix_client_hydrogen_docker_image: "{{ matrix_client_hydrogen_docker_image_name_prefix }}vectorim/hydrogen-web:{{ matrix_client_hydrogen_version }}"
+matrix_client_hydrogen_docker_image_name_prefix: "{{ 'localhost/' if matrix_client_hydrogen_container_image_self_build }}"
+matrix_client_hydrogen_docker_image_force_pull: "{{ matrix_client_hydrogen_docker_image.endswith(':latest') }}"
+
+matrix_client_hydrogen_data_path: "{{ matrix_base_data_path }}/client-hydrogen"
+matrix_client_hydrogen_docker_src_files_path: "{{ matrix_client_hydrogen_data_path }}/docker-src"
+
+# Controls whether the container exposes its HTTP port (tcp/8080 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8768"), or empty string to not expose.
+matrix_client_hydrogen_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_client_hydrogen_container_extra_arguments: []
+
+# List of systemd services that matrix-client-hydrogen.service depends on
+matrix_client_hydrogen_systemd_required_services_list: ['docker.service']
+
+# Controls whether the self-check feature should validate SSL certificates.
+matrix_client_hydrogen_self_check_validate_certificates: true
+
+# config.json
+matrix_client_hydrogen_default_hs_url: ""
+
+# Default Hydrogen configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_client_hydrogen_configuration_extension_json`)
+# or completely replace this variable with your own template.
+#
+# The side-effect of this lookup is that Ansible would even parse the JSON for us, returning a dict.
+# This is unlike what it does when looking up YAML template files (no automatic parsing there).
+matrix_client_hydrogen_configuration_default: "{{ lookup('template', 'templates/config.json.j2') }}"
+
+# Your custom JSON configuration for Hydrogen should go to `matrix_client_hydrogen_configuration_extension_json`.
+# This configuration extends the default starting configuration (`matrix_client_hydrogen_configuration_default`).
+#
+# You can override individual variables from the default configuration, or introduce new ones.
+#
+# If you need something more special, you can take full control by
+# completely redefining `matrix_client_hydrogen_configuration_default`.
+#
+# Example configuration extension follows:
+#
+# matrix_client_hydrogen_configuration_extension_json: |
+#  {
+#    "push": {
+#      "appId": "io.element.hydrogen.web",
+#      "gatewayUrl": "https://matrix.org",
+#      "applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM"
+#    },
+#    "defaultHomeServer": "matrix.org"
+#  }
+matrix_client_hydrogen_configuration_extension_json: '{}'
+
+matrix_client_hydrogen_configuration_extension: "{{ matrix_client_hydrogen_configuration_extension_json|from_json if matrix_client_hydrogen_configuration_extension_json|from_json is mapping else {} }}"
+
+# Holds the final Hydrogen configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_client_hydrogen_configuration_default`.
+matrix_client_hydrogen_configuration: "{{ matrix_client_hydrogen_configuration_default|combine(matrix_client_hydrogen_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-client-hydrogen/tasks/init.yml b/roles/matrix-client-hydrogen/tasks/init.yml
new file mode 100644
index 000000000..8116a0034
--- /dev/null
+++ b/roles/matrix-client-hydrogen/tasks/init.yml
@@ -0,0 +1,10 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Hydrogen image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_client_hydrogen_container_image_self_build and matrix_client_hydrogen_enabled"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-client-hydrogen.service'] }}"
+  when: matrix_client_hydrogen_enabled|bool
diff --git a/roles/matrix-client-hydrogen/tasks/main.yml b/roles/matrix-client-hydrogen/tasks/main.yml
new file mode 100644
index 000000000..6534db05d
--- /dev/null
+++ b/roles/matrix-client-hydrogen/tasks/main.yml
@@ -0,0 +1,15 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_client_hydrogen_enabled|bool"
+  tags:
+    - setup-all
+    - setup-client-hydrogen
+
+- import_tasks: "{{ role_path }}/tasks/setup.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-client-hydrogen
diff --git a/roles/matrix-client-hydrogen/tasks/self_check.yml b/roles/matrix-client-hydrogen/tasks/self_check.yml
new file mode 100644
index 000000000..c7407dcd5
--- /dev/null
+++ b/roles/matrix-client-hydrogen/tasks/self_check.yml
@@ -0,0 +1,22 @@
+---
+
+- set_fact:
+    matrix_client_hydrogen_url_endpoint_public: "https://{{ matrix_server_fqn_hydrogen }}"
+
+- name: Check Hydrogen
+  uri:
+    url: "{{ matrix_client_hydrogen_url_endpoint_public }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_client_hydrogen_self_check_validate_certificates }}"
+  register: matrix_client_hydrogen_self_check_result
+  check_mode: no
+  ignore_errors: true
+
+- name: Fail if Hydrogen not working
+  fail:
+    msg: "Failed checking Hydrogen is up at `{{ matrix_server_fqn_hydrogen }}` (checked endpoint: `{{ matrix_client_hydrogen_url_endpoint_public }}`). Is Hydrogen running? Is port 443 open in your firewall? Full error: {{ matrix_client_hydrogen_self_check_result }}"
+  when: "matrix_client_hydrogen_self_check_result.failed or 'json' not in matrix_client_hydrogen_self_check_result"
+
+- name: Report working Hydrogen
+  debug:
+    msg: "Hydrogen at `{{ matrix_server_fqn_hydrogen }}` is working (checked endpoint: `{{ matrix_client_hydrogen_url_endpoint_public }}`)"
diff --git a/roles/matrix-client-hydrogen/tasks/setup.yml b/roles/matrix-client-hydrogen/tasks/setup.yml
new file mode 100644
index 000000000..205fa3ceb
--- /dev/null
+++ b/roles/matrix-client-hydrogen/tasks/setup.yml
@@ -0,0 +1,119 @@
+---
+
+#
+# Tasks related to setting up Hydrogen
+#
+
+- name: Ensure Hydrogen paths exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_client_hydrogen_data_path }}", when: true }
+    - { path: "{{ matrix_client_hydrogen_docker_src_files_path }}", when: "{{ matrix_client_hydrogen_container_image_self_build }}" }
+  when: matrix_client_hydrogen_enabled|bool and item.when
+
+- name: Ensure Hydrogen Docker image is pulled
+  docker_image:
+   name: "{{ matrix_client_hydrogen_docker_image }}"
+   source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+   force_source: "{{ matrix_client_hydrogen_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+   force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_client_hydrogen_docker_image_force_pull }}"
+  when: matrix_client_hydrogen_enabled|bool and not matrix_client_hydrogen_container_image_self_build
+
+- name: Ensure Hydrogen repository is present on self-build
+  git:
+    repo: "{{ matrix_client_hydrogen_container_image_self_build_repo }}"
+    dest: "{{ matrix_client_hydrogen_docker_src_files_path }}"
+    version: "{{ matrix_client_hydrogen_docker_image.split(':')[1] }}"
+    force: "yes"
+  register: matrix_client_hydrogen_git_pull_results
+  when: "matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_container_image_self_build|bool"
+
+- name: Ensure Hydrogen configuration installed
+  copy:
+    content: "{{ matrix_client_hydrogen_configuration|to_nice_json }}"
+    dest: "{{ matrix_client_hydrogen_docker_src_files_path }}/assets/config.json"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_container_image_self_build|bool"
+
+- name: Ensure Hydrogen additional config files installed
+  template:
+    src: "{{ item.src }}"
+    dest: "{{ matrix_client_hydrogen_data_path }}/{{ item.name }}"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - {src: "{{ role_path }}/templates/nginx.conf.j2", name: "nginx.conf"}
+  when: "matrix_client_hydrogen_enabled|bool and item.src is not none"
+
+- name: Ensure Hydrogen Docker image is built
+  docker_image:
+    name: "{{ matrix_client_hydrogen_docker_image }}"
+    source: build
+    force_source: "{{ matrix_client_hydrogen_git_pull_results.changed }}"
+    build:
+      dockerfile: Dockerfile
+      path: "{{ matrix_client_hydrogen_docker_src_files_path }}"
+      pull: yes
+  when: "matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_container_image_self_build|bool"
+
+- name: Ensure matrix-client-hydrogen.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-client-hydrogen.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-client-hydrogen.service"
+    mode: 0644
+  register: matrix_client_hydrogen_systemd_service_result
+  when: matrix_client_hydrogen_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-client-hydrogen.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_client_hydrogen_enabled and matrix_client_hydrogen_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of Hydrogen (if it was previously enabled)
+#
+
+- name: Check existence of matrix-client-hydrogen.service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-client-hydrogen.service"
+  register: matrix_client_hydrogen_service_stat
+  when: "not matrix_client_hydrogen_enabled|bool"
+
+- name: Ensure matrix-client-hydrogen is stopped
+  service:
+    name: matrix-client-hydrogen
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_service_stat.stat.exists"
+
+- name: Ensure matrix-client-hydrogen.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-client-hydrogen.service"
+    state: absent
+  when: "not matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-client-hydrogen.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_client_hydrogen_enabled|bool and matrix_client_hydrogen_service_stat.stat.exists"
+
+- name: Ensure Hydrogen paths doesn't exist
+  file:
+    path: "{{ matrix_client_hydrogen_data_path }}"
+    state: absent
+  when: "not matrix_client_hydrogen_enabled|bool"
+
+- name: Ensure Hydrogen Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_client_hydrogen_docker_image }}"
+    state: absent
+  when: "not matrix_client_hydrogen_enabled|bool"
diff --git a/roles/matrix-client-hydrogen/tasks/validate_config.yml b/roles/matrix-client-hydrogen/tasks/validate_config.yml
new file mode 100644
index 000000000..d3b9a709b
--- /dev/null
+++ b/roles/matrix-client-hydrogen/tasks/validate_config.yml
@@ -0,0 +1,9 @@
+---
+
+- name: Fail if required Hydrogen settings not defined
+  fail:
+    msg: >
+      You need to define a required configuration setting (`{{ item }}`) to use Hydrogen.
+  when: "(vars[item] == '' or vars[item] is none) and matrix_client_hydrogen_container_image_self_build|bool"
+  with_items:
+    - "matrix_client_hydrogen_default_hs_url"
diff --git a/roles/matrix-client-hydrogen/templates/config.json.j2 b/roles/matrix-client-hydrogen/templates/config.json.j2
new file mode 100644
index 000000000..62a849b0f
--- /dev/null
+++ b/roles/matrix-client-hydrogen/templates/config.json.j2
@@ -0,0 +1,3 @@
+{
+	"defaultHomeServer": {{ matrix_client_hydrogen_default_hs_url|string|to_json }}
+}
diff --git a/roles/matrix-client-hydrogen/templates/nginx.conf.j2 b/roles/matrix-client-hydrogen/templates/nginx.conf.j2
new file mode 100644
index 000000000..fba16bbdc
--- /dev/null
+++ b/roles/matrix-client-hydrogen/templates/nginx.conf.j2
@@ -0,0 +1,66 @@
+#jinja2: lstrip_blocks: "True"
+# This is a custom nginx configuration file that we use in the container (instead of the default one),
+# because it allows us to run nginx with a non-root user.
+#
+# For this to work, the default vhost file (`/etc/nginx/conf.d/default.conf`) also needs to be removed.
+# (mounting `/dev/null` over `/etc/nginx/conf.d/default.conf` works well)
+#
+# The following changes have been done compared to a default nginx configuration file:
+# - default server port is changed (80 -> 8080), so that a non-root user can bind it
+# - various temp paths are changed to `/tmp`, so that a non-root user can write to them
+# - the `user` directive was removed, as we don't want nginx to switch users
+
+worker_processes 1;
+
+error_log /var/log/nginx/error.log warn;
+pid /tmp/nginx.pid;
+
+
+events {
+	worker_connections 1024;
+}
+
+
+http {
+	proxy_temp_path /tmp/proxy_temp;
+	client_body_temp_path /tmp/client_temp;
+	fastcgi_temp_path /tmp/fastcgi_temp;
+	uwsgi_temp_path /tmp/uwsgi_temp;
+	scgi_temp_path /tmp/scgi_temp;
+
+	include /etc/nginx/mime.types;
+	default_type application/octet-stream;
+
+	log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+					'$status $body_bytes_sent "$http_referer" '
+					'"$http_user_agent" "$http_x_forwarded_for"';
+
+	access_log /var/log/nginx/access.log main;
+
+	sendfile on;
+	#tcp_nopush on;
+
+	keepalive_timeout 65;
+
+	#gzip on;
+
+	server {
+		listen 8080;
+		server_name localhost;
+
+		root /usr/share/nginx/html;
+
+		location / {
+			index index.html index.htm;
+		}
+
+		location ~* ^/(config(.+)?\.json$|(.+)\.html$|i18n) {
+			expires -1;
+		}
+
+		error_page 500 502 503 504 /50x.html;
+		location = /50x.html {
+			root /usr/share/nginx/html;
+		}
+	}
+}
diff --git a/roles/matrix-client-hydrogen/templates/systemd/matrix-client-hydrogen.service.j2 b/roles/matrix-client-hydrogen/templates/systemd/matrix-client-hydrogen.service.j2
new file mode 100644
index 000000000..c85aeb978
--- /dev/null
+++ b/roles/matrix-client-hydrogen/templates/systemd/matrix-client-hydrogen.service.j2
@@ -0,0 +1,39 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Hydrogen Client
+{% for service in matrix_client_hydrogen_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-client-hydrogen 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-client-hydrogen 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-client-hydrogen \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_client_hydrogen_container_http_host_bind_port %}
+			-p {{ matrix_client_hydrogen_container_http_host_bind_port }}:8080 \
+			{% endif %}
+			--tmpfs=/tmp:rw,noexec,nosuid,size=10m \
+			--mount type=bind,src={{ matrix_client_hydrogen_data_path }}/nginx.conf,dst=/etc/nginx/nginx.conf,ro \
+			{% for arg in matrix_client_hydrogen_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_client_hydrogen_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-client-hydrogen 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-client-hydrogen 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-client-hydrogen
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-dynamic-dns/defaults/main.yml b/roles/matrix-dynamic-dns/defaults/main.yml
new file mode 100644
index 000000000..3411d0f83
--- /dev/null
+++ b/roles/matrix-dynamic-dns/defaults/main.yml
@@ -0,0 +1,48 @@
+# Whether dynamic dns is enabled
+matrix_dynamic_dns_enabled: true
+
+# The dynamic dns daemon interval
+matrix_dynamic_dns_daemon_interval: '300'
+
+matrix_dynamic_dns_version: v3.9.1-ls45
+
+# The docker container to use when in mode
+matrix_dynamic_dns_docker_image: "{{ matrix_dynamic_dns_docker_image_name_prefix }}linuxserver/ddclient:{{ matrix_dynamic_dns_version }}"
+
+matrix_dynamic_dns_docker_image_name_prefix: "{{ 'localhost/' if matrix_dynamic_dns_container_image_self_build else matrix_container_global_registry_prefix }}"
+
+# The image to force pull
+matrix_dynamic_dns_docker_image_force_pull: "{{ matrix_dynamic_dns_docker_image.endswith(':latest') }}"
+
+# List of extra arguments to pass to the ontainer mode
+matrix_dynamic_dns_container_extra_arguments: []
+
+# List of wanted services when running in mode
+matrix_dynamic_dns_systemd_wanted_services_list: []
+
+# List of required services when running in mode
+matrix_dynamic_dns_systemd_required_services_list: ['docker.service']
+
+# Build the container from source when running in mode
+matrix_dynamic_dns_container_image_self_build: false
+matrix_dynamic_dns_container_image_self_build_repo: "https://github.com/linuxserver/docker-ddclient.git"
+
+# Config paths
+matrix_dynamic_dns_base_path: "{{ matrix_base_data_path }}/dynamic-dns"
+matrix_dynamic_dns_config_path: "{{ matrix_dynamic_dns_base_path }}/config"
+matrix_dynamic_dns_docker_src_files_path: "{{ matrix_dynamic_dns_base_path }}/docker-src"
+
+# Holds the configurations (the domains to update DNS for, the providers they use, etc.)
+#
+# Example:
+# matrix_dynamic_dns_domain_configurations:
+#   - provider: domains.google.com
+#     protocol: dyndn2
+#     username: XXXXXXXXXXXXXXXX
+#     password: XXXXXXXXXXXXXXXX
+#     domain: "{{ matrix_domain }}"
+matrix_dynamic_dns_domain_configurations: []
+
+# Config options
+matrix_dynamic_dns_additional_configuration_blocks: []
+matrix_dynamic_dns_use: "web"
diff --git a/roles/matrix-dynamic-dns/tasks/init.yml b/roles/matrix-dynamic-dns/tasks/init.yml
new file mode 100644
index 000000000..e7d33ff28
--- /dev/null
+++ b/roles/matrix-dynamic-dns/tasks/init.yml
@@ -0,0 +1,10 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_dynamic_dns_container_image_self_build and matrix_dynamic_dns_enabled"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-dynamic-dns.service'] }}"
+  when: "matrix_dynamic_dns_enabled|bool"
diff --git a/roles/matrix-dynamic-dns/tasks/install.yml b/roles/matrix-dynamic-dns/tasks/install.yml
new file mode 100644
index 000000000..ac69ec896
--- /dev/null
+++ b/roles/matrix-dynamic-dns/tasks/install.yml
@@ -0,0 +1,62 @@
+---
+
+- name: Ensure Dynamic DNS image is pulled
+  docker_image:
+    name: "{{ matrix_dynamic_dns_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_dynamic_dns_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_dynamic_dns_docker_image_force_pull }}"
+  when: matrix_dynamic_dns_enabled|bool and not matrix_dynamic_dns_container_image_self_build
+
+- name: Ensure Dynamic DNS paths exist
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0751
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_dynamic_dns_base_path }}", when: true }
+    - { path: "{{ matrix_dynamic_dns_config_path }}", when: true }
+    - { path: "{{ matrix_dynamic_dns_docker_src_files_path }}", when: "{{ matrix_dynamic_dns_container_image_self_build }}" }
+  when: matrix_dynamic_dns_enabled|bool and item.when|bool
+
+- name: Ensure Dynamic DNS repository is present on self build
+  git:
+    repo: "{{ matrix_dynamic_dns_container_image_self_build_repo }}"
+    dest: "{{ matrix_dynamic_dns_docker_src_files_path }}"
+    force: "yes"
+  register: matrix_dynamic_dns_git_pull_results
+  when: "matrix_dynamic_dns_enabled|bool and matrix_dynamic_dns_container_image_self_build|bool"
+
+- name: Ensure Dynamic DNS Docker image is built
+  docker_image:
+    name: "{{ matrix_dynamic_dns_docker_image }}"
+    source: build
+    force_source: "{{ matrix_dynamic_dns_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_dynamic_dns_git_pull_results.changed }}"
+    build:
+      dockerfile: Dockerfile
+      path: "{{ matrix_dynamic_dns_docker_src_files_path }}"
+      pull: yes
+  when: "matrix_dynamic_dns_enabled|bool and matrix_dynamic_dns_container_image_self_build|bool"
+
+- name: Ensure Dynamic DNS ddclient.conf installed
+  template:
+    src: "{{ role_path }}/templates/ddclient.conf.j2"
+    dest: "{{ matrix_dynamic_dns_config_path }}/ddclient.conf"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-dynamic-dns.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-dynamic-dns.service.j2"
+    dest: "/etc/systemd/system/matrix-dynamic-dns.service"
+    mode: 0644
+  register: matrix_dynamic_dns_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-dynamic-dns.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_dynamic_dns_systemd_service_result.changed"
diff --git a/roles/matrix-dynamic-dns/tasks/main.yml b/roles/matrix-dynamic-dns/tasks/main.yml
new file mode 100644
index 000000000..f9aaab8f5
--- /dev/null
+++ b/roles/matrix-dynamic-dns/tasks/main.yml
@@ -0,0 +1,21 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_dynamic_dns_enabled|bool"
+  tags:
+    - setup-all
+    - setup-dynamic-dns
+
+- import_tasks: "{{ role_path }}/tasks/install.yml"
+  when: "run_setup|bool and matrix_dynamic_dns_enabled|bool"
+  tags:
+    - setup-all
+    - setup-dynamic-dns
+
+- import_tasks: "{{ role_path }}/tasks/uninstall.yml"
+  when: "run_setup|bool and not matrix_dynamic_dns_enabled|bool"
+  tags:
+    - setup-all
+    - setup-dynamic-dns
diff --git a/roles/matrix-dynamic-dns/tasks/uninstall.yml b/roles/matrix-dynamic-dns/tasks/uninstall.yml
new file mode 100644
index 000000000..f3caba256
--- /dev/null
+++ b/roles/matrix-dynamic-dns/tasks/uninstall.yml
@@ -0,0 +1,27 @@
+---
+
+- name: Check existence of matrix-dynamic-dns service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-dynamic-dns.service"
+  register: matrix_dynamic_dns_service_stat
+
+- name: Ensure matrix-dynamic-dns is stopped
+  service:
+    name: matrix-dynamic-dns
+    state: stopped
+    daemon_reload: yes
+  when: "matrix_dynamic_dns_service_stat.stat.exists"
+
+- name: Ensure matrix-dynamic-dns.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-dynamic-dns.service"
+    state: absent
+  when: "matrix_dynamic_dns_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-dynamic-dns.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_dynamic_dns_service_stat.stat.exists"
+
+# Intentionally not removing the Docker image when uninstalling.
+# We can't be sure it had been pulled by us in the first place.
diff --git a/roles/matrix-dynamic-dns/tasks/validate_config.yml b/roles/matrix-dynamic-dns/tasks/validate_config.yml
new file mode 100644
index 000000000..8f0001eaa
--- /dev/null
+++ b/roles/matrix-dynamic-dns/tasks/validate_config.yml
@@ -0,0 +1,16 @@
+---
+
+- name: Fail if no configurations specified
+  fail:
+    msg: >-
+      You need to define at least one configuration in `matrix_dynamic_dns_domain_configurations` for using matrix-dynamic-dns.
+  when: "matrix_dynamic_dns_domain_configurations|length == 0"
+
+- name: Fail if required settings not defined in configuration blocks
+  fail:
+    msg: >-
+      One of the configurations in matrix_dynamic_dns_domain_configurations is missing a required key (domain, provider, protocol).
+  when: "'domain' not in configuration or 'provider' not in configuration or 'protocol' not in configuration"
+  with_items: "{{ matrix_dynamic_dns_domain_configurations }}"
+  loop_control:
+    loop_var: configuration
diff --git a/roles/matrix-dynamic-dns/templates/ddclient.conf.j2 b/roles/matrix-dynamic-dns/templates/ddclient.conf.j2
new file mode 100644
index 000000000..1480d834e
--- /dev/null
+++ b/roles/matrix-dynamic-dns/templates/ddclient.conf.j2
@@ -0,0 +1,26 @@
+daemon={{ matrix_dynamic_dns_daemon_interval }}
+syslog=no
+pid=/var/run/ddclient/ddclient.pid
+ssl=yes
+use={{ matrix_dynamic_dns_use }}
+
+{% for dynamic_dns_domain_configuration in matrix_dynamic_dns_domain_configurations %}
+protocol={{ dynamic_dns_domain_configuration.protocol }}
+server={{ dynamic_dns_domain_configuration.provider }} {% if 'username' in dynamic_dns_domain_configuration %}
+login='{{ dynamic_dns_domain_configuration.username }}' {% endif %} {% if 'password' in dynamic_dns_domain_configuration %}
+password='{{ dynamic_dns_domain_configuration.password }}' {% endif %} {% if 'static' in dynamic_dns_domain_configuration %}
+static=yes {% endif %} {% if 'custom' in dynamic_dns_domain_configuration %}
+custom=yes {% endif %} {% if 'zone' in dynamic_dns_domain_configuration %}
+zone={{ dynamic_dns_domain_configuration.zone }} {% endif %} {% if 'ttl' in dynamic_dns_domain_configuration %}
+ttl={{ dynamic_dns_domain_configuration.ttl }} {% endif %} {% if 'mx' in dynamic_dns_domain_configuration %}
+mx={{ dynamic_dns_domain_configuration.mx }} {% endif %} {% if 'wildcard' in dynamic_dns_domain_configuration %}
+wildcard=yes {% endif %}
+{{ dynamic_dns_domain_configuration.domain }}
+
+{% endfor %}
+
+
+{% for matrix_dynamic_dns_additional_configuration in matrix_dynamic_dns_additional_configuration_blocks %}
+{{ matrix_dynamic_dns_additional_configuration }}
+
+{% endfor %}
diff --git a/roles/matrix-dynamic-dns/templates/systemd/matrix-dynamic-dns.service.j2 b/roles/matrix-dynamic-dns/templates/systemd/matrix-dynamic-dns.service.j2
new file mode 100644
index 000000000..dfdd2f72c
--- /dev/null
+++ b/roles/matrix-dynamic-dns/templates/systemd/matrix-dynamic-dns.service.j2
@@ -0,0 +1,36 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Dynamic DNS
+{% for service in matrix_dynamic_dns_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_dynamic_dns_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-dynamic-dns 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-dynamic-dns 2>/dev/null'
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-dynamic-dns \
+			--log-driver=none \
+			--network={{ matrix_docker_network }} \
+  			-e PUID={{ matrix_user_uid }} \
+  			-e PGID={{ matrix_user_gid }} \
+			-v {{ matrix_dynamic_dns_config_path }}:/config:z \
+			{% for arg in matrix_dynamic_dns_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_dynamic_dns_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-dynamic-dns 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-dynamic-dns 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-dynamic-dns
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-email2matrix/defaults/main.yml b/roles/matrix-email2matrix/defaults/main.yml
new file mode 100644
index 000000000..e6bfa0fe6
--- /dev/null
+++ b/roles/matrix-email2matrix/defaults/main.yml
@@ -0,0 +1,44 @@
+matrix_email2matrix_enabled: true
+
+matrix_email2matrix_base_path: "{{ matrix_base_data_path }}/email2matrix"
+matrix_email2matrix_config_dir_path: "{{ matrix_email2matrix_base_path }}/config"
+
+matrix_email2matrix_version: 1.0.1
+matrix_email2matrix_docker_image: "{{ matrix_container_global_registry_prefix }}devture/email2matrix:{{ matrix_email2matrix_version }}"
+matrix_email2matrix_docker_image_force_pull: "{{ matrix_email2matrix_docker_image.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_email2matrix_container_extra_arguments: []
+
+# List of systemd services that matrix-corporal.service depends on
+matrix_email2matrix_systemd_required_services_list: ['docker.service']
+
+# Controls where the matrix-email2matrix container exposes the SMTP (tcp/2525 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:2525").
+#
+# By default, we listen on port 25 on all of the host's network interfaces.
+matrix_email2matrix_smtp_host_bind_port: "25"
+
+matrix_email2matrix_smtp_hostname: "{{ matrix_server_fqn_matrix }}"
+
+# A list of mailbox to Matrix mappings.
+#
+# Example:
+# matrix_email2matrix_matrix_mappings:
+#  - MailboxName: "mailbox1"
+#    MatrixRoomId: "!bpcwlxIUxVvvgXcbjy:example.com"
+#    MatrixHomeserverUrl: "{{ matrix_homeserver_url }}"
+#    MatrixUserId": "@email2matrix:{{ matrix_domain }}"
+#    MatrixAccessToken": "TOKEN_HERE"
+#    IgnoreSubject: false
+#
+#  - MailboxName: "mailbox2"
+#    MatrixRoomId: "!another:example.com"
+#    MatrixHomeserverUrl: "{{ matrix_homeserver_url }}"
+#    MatrixUserId": "@email2matrix:{{ matrix_domain }}"
+#    MatrixAccessToken": "TOKEN_HERE"
+#    IgnoreSubject: true
+matrix_email2matrix_matrix_mappings: []
+
+matrix_email2matrix_misc_debug: false
diff --git a/roles/matrix-email2matrix/tasks/init.yml b/roles/matrix-email2matrix/tasks/init.yml
new file mode 100644
index 000000000..0c8ffc0cd
--- /dev/null
+++ b/roles/matrix-email2matrix/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-email2matrix.service'] }}"
+  when: matrix_email2matrix_enabled|bool
diff --git a/roles/matrix-email2matrix/tasks/main.yml b/roles/matrix-email2matrix/tasks/main.yml
new file mode 100644
index 000000000..231146730
--- /dev/null
+++ b/roles/matrix-email2matrix/tasks/main.yml
@@ -0,0 +1,15 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_email2matrix_enabled|bool"
+  tags:
+    - setup-all
+    - setup-email2matrix
+
+- import_tasks: "{{ role_path }}/tasks/setup_email2matrix.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-email2matrix
diff --git a/roles/matrix-email2matrix/tasks/setup_email2matrix.yml b/roles/matrix-email2matrix/tasks/setup_email2matrix.yml
new file mode 100644
index 000000000..d5fa73a51
--- /dev/null
+++ b/roles/matrix-email2matrix/tasks/setup_email2matrix.yml
@@ -0,0 +1,88 @@
+---
+
+#
+# Tasks related to setting up Email2Matrix
+#
+
+- name: Ensure Email2Matrix paths exist
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_email2matrix_base_path }}"
+    - "{{ matrix_email2matrix_config_dir_path }}"
+  when: matrix_email2matrix_enabled|bool
+
+- name: Ensure Email2Matrix configuration file created
+  template:
+    src: "{{ role_path }}/templates/config.json.j2"
+    dest: "{{ matrix_email2matrix_config_dir_path }}/config.json"
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+    mode: 0640
+  when: matrix_email2matrix_enabled|bool
+
+- name: Ensure Email2Matrix image is pulled
+  docker_image:
+    name: "{{ matrix_email2matrix_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_email2matrix_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_email2matrix_docker_image_force_pull }}"
+  when: matrix_email2matrix_enabled|bool
+
+- name: Ensure matrix-email2matrix.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-email2matrix.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-email2matrix.service"
+    mode: 0644
+  register: matrix_email2matrix_systemd_service_result
+  when: matrix_email2matrix_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-email2matrix.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_email2matrix_enabled|bool and matrix_email2matrix_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of the Email2Matrix (if it was previously enabled)
+#
+
+- name: Check existence of matrix-email2matrix service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-email2matrix.service"
+  register: matrix_email2matrix_service_stat
+  when: "not matrix_email2matrix_enabled|bool"
+
+- name: Ensure matrix-email2matrix is stopped
+  service:
+    name: matrix-email2matrix
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_email2matrix_enabled|bool and matrix_email2matrix_service_stat.stat.exists"
+
+- name: Ensure matrix-email2matrix.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-email2matrix.service"
+    state: absent
+  when: "not matrix_email2matrix_enabled|bool and matrix_email2matrix_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-email2matrix.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_email2matrix_enabled|bool and matrix_email2matrix_service_stat.stat.exists"
+
+- name: Ensure Email2Matrix data path doesn't exist
+  file:
+    path: "{{ matrix_email2matrix_base_path }}"
+    state: absent
+  when: "not matrix_email2matrix_enabled|bool"
+
+- name: Ensure Email2Matrix Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_email2matrix_docker_image }}"
+    state: absent
+  when: "not matrix_email2matrix_enabled|bool"
diff --git a/roles/matrix-email2matrix/tasks/validate_config.yml b/roles/matrix-email2matrix/tasks/validate_config.yml
new file mode 100644
index 000000000..d8beecf4a
--- /dev/null
+++ b/roles/matrix-email2matrix/tasks/validate_config.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Fail if no mappings
+  fail:
+    msg: >
+      You need to define at least one mapping in `matrix_email2matrix_matrix_mappings` for enabling Email2Matrix.
+  when: "matrix_email2matrix_matrix_mappings|length == 0"
diff --git a/roles/matrix-email2matrix/templates/config.json.j2 b/roles/matrix-email2matrix/templates/config.json.j2
new file mode 100644
index 000000000..c1be97fdb
--- /dev/null
+++ b/roles/matrix-email2matrix/templates/config.json.j2
@@ -0,0 +1,14 @@
+#jinja2: lstrip_blocks: "True"
+{
+	"Smtp": {
+		"ListenInterface": "0.0.0.0:2525",
+		"Hostname": {{ matrix_email2matrix_smtp_hostname|to_json }},
+		"Workers": 10
+	},
+	"Matrix": {
+		"Mappings": {{ matrix_email2matrix_matrix_mappings|to_nice_json }}
+	},
+	"Misc": {
+		"Debug": {{ matrix_email2matrix_misc_debug|to_json }}
+	}
+}
diff --git a/roles/matrix-email2matrix/templates/systemd/matrix-email2matrix.service.j2 b/roles/matrix-email2matrix/templates/systemd/matrix-email2matrix.service.j2
new file mode 100644
index 000000000..c92267682
--- /dev/null
+++ b/roles/matrix-email2matrix/templates/systemd/matrix-email2matrix.service.j2
@@ -0,0 +1,34 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Email2Matrix
+After=docker.service
+Requires=docker.service
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-email2matrix 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-email2matrix 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-email2matrix \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--network={{ matrix_docker_network }} \
+			-p {{ matrix_email2matrix_smtp_host_bind_port }}:2525 \
+			--mount type=bind,src={{ matrix_email2matrix_config_dir_path }}/config.json,dst=/config.json,ro \
+			{% for arg in matrix_email2matrix_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_email2matrix_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-email2matrix 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-email2matrix 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-email2matrix
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-etherpad/defaults/main.yml b/roles/matrix-etherpad/defaults/main.yml
new file mode 100644
index 000000000..45f8f8b28
--- /dev/null
+++ b/roles/matrix-etherpad/defaults/main.yml
@@ -0,0 +1,87 @@
+matrix_etherpad_enabled: false
+
+matrix_etherpad_base_path: "{{ matrix_base_data_path }}/etherpad"
+
+matrix_etherpad_version: 1.8.12
+matrix_etherpad_docker_image: "{{ matrix_container_global_registry_prefix }}etherpad/etherpad:{{ matrix_etherpad_version }}"
+matrix_etherpad_docker_image_force_pull: "{{ matrix_etherpad_docker_image.endswith(':latest') }}"
+
+# List of systemd services that matrix-etherpad.service depends on.
+matrix_etherpad_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-etherpad.service wants
+matrix_etherpad_systemd_wanted_services_list: []
+
+# Container user has to be able to write to the source file directories until this bug is fixed:
+# https://github.com/ether/etherpad-lite/issues/2683
+matrix_etherpad_user_uid: '5001'
+matrix_etherpad_user_gid: '5001'
+
+# Controls whether the matrix-etherpad container exposes its HTTP port (tcp/9001 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:9001"), or empty string to not expose.
+matrix_etherpad_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_etherpad_container_extra_arguments: []
+
+matrix_etherpad_public_endpoint: '/etherpad'
+
+# By default, the Etherpad app can be accessed within the Dimension domain
+matrix_etherpad_base_url: "https://{{ matrix_server_fqn_dimension }}{{ matrix_etherpad_public_endpoint }}"
+
+# Database-related configuration fields.
+#
+# Etherpad requires a dedicated database
+matrix_etherpad_database_engine: 'postgres'
+
+matrix_etherpad_database_username: 'matrix_etherpad'
+matrix_etherpad_database_password: 'some-password'
+matrix_etherpad_database_hostname: 'matrix-postgres'
+matrix_etherpad_database_port: 5432
+matrix_etherpad_database_name: 'matrix_etherpad'
+
+matrix_etherpad_database_connection_string: 'postgres://{{ matrix_etherpad_database_username }}:{{ matrix_etherpad_database_password }}@{{ matrix_etherpad_database_hostname }}:{{ matrix_etherpad_database_port }}/{{ matrix_etherpad_database_name }}'
+
+# Variables configuring the etherpad
+matrix_etherpad_title: 'Etherpad'
+matrix_etherpad_default_pad_text: |
+  Welcome to Etherpad!
+
+  This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!
+
+  Get involved with Etherpad at https://etherpad.org
+
+# Default Etherpad configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_etherpad_configuration_extension_json`)
+# or completely replace this variable with your own template.
+matrix_etherpad_configuration_default: "{{ lookup('template', 'templates/settings.json.j2') }}"
+
+# Your custom JSON configuration for Etherpad goes here.
+# This configuration extends the default starting configuration (`matrix_etherpad_configuration_json`).
+#
+# You can override individual variables from the default configuration, or introduce new ones.
+#
+# If you need something more special, you can take full control by
+# completely redefining `matrix_etherpad_configuration_json`.
+#
+# Example configuration extension follows:
+#
+# matrix_etherpad_configuration_extension_json: |
+#  {
+#   "loadTest": true,
+#   "commitRateLimiting": {
+#     "duration": 1,
+#     "points": 10
+#   }
+#  }
+#
+matrix_etherpad_configuration_extension_json: '{}'
+
+matrix_etherpad_configuration_extension: "{{ matrix_etherpad_configuration_extension_json|from_json if matrix_etherpad_configuration_extension_json|from_json is mapping else {} }}"
+
+# Holds the final Etherpad configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_etherpad_configuration_json`.
+matrix_etherpad_configuration: "{{ matrix_etherpad_configuration_default|combine(matrix_etherpad_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-etherpad/tasks/init.yml b/roles/matrix-etherpad/tasks/init.yml
new file mode 100644
index 000000000..081d4c233
--- /dev/null
+++ b/roles/matrix-etherpad/tasks/init.yml
@@ -0,0 +1,62 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-etherpad.service'] }}"
+  when: matrix_etherpad_enabled|bool
+
+- block:
+  - name: Fail if matrix-nginx-proxy role already executed
+    fail:
+      msg: >-
+        Trying to append Etherpad's reverse-proxying configuration to matrix-nginx-proxy,
+        but it's pointless since the matrix-nginx-proxy role had already executed.
+        To fix this, please change the order of roles in your plabook,
+        so that the matrix-nginx-proxy role would run after the matrix-etherpad role.
+    when: matrix_nginx_proxy_role_executed|default(False)|bool
+
+  - name: Generate Etherpad proxying configuration for matrix-nginx-proxy
+    set_fact:
+      matrix_etherpad_matrix_nginx_proxy_configuration: |
+        rewrite ^{{ matrix_etherpad_public_endpoint }}$ $scheme://$server_name{{ matrix_etherpad_public_endpoint }}/ permanent;
+
+        location {{ matrix_etherpad_public_endpoint }}/ {
+        {% if matrix_nginx_proxy_enabled|default(False) %}
+          {# Use the embedded DNS resolver in Docker containers to discover the service #}
+          resolver 127.0.0.11 valid=5s;
+          proxy_pass http://matrix-etherpad:9001/;
+          {# These are proxy directives needed specifically by Etherpad #}
+          proxy_buffering off;
+          proxy_http_version 1.1;  # recommended with keepalive connections
+          proxy_pass_header Server;
+          proxy_set_header Host $host;
+          proxy_set_header X-Forwarded-Proto $scheme; # for EP to set secure cookie flag when https is used
+          # WebSocket proxying - from http://nginx.org/en/docs/http/websocket.html
+          proxy_set_header Upgrade $http_upgrade;
+          proxy_set_header Connection $connection_upgrade;
+        {% else %}
+          {# Generic configuration for use outside of our container setup #}
+          # A good guide for setting up your Etherpad behind nginx:
+          # https://docs.gandi.net/en/cloud/tutorials/etherpad_lite.html
+          proxy_pass http://127.0.0.1:9001/;
+        {% endif %}
+        }
+
+  - name: Register Etherpad proxying configuration with matrix-nginx-proxy
+    set_fact:
+      matrix_nginx_proxy_proxy_dimension_additional_server_configuration_blocks: |
+        {{
+          matrix_nginx_proxy_proxy_dimension_additional_server_configuration_blocks|default([])
+          +
+          [matrix_etherpad_matrix_nginx_proxy_configuration]
+        }}
+  tags:
+    - always
+  when: matrix_etherpad_enabled|bool
+
+- name: Warn about reverse-proxying if matrix-nginx-proxy not used
+  debug:
+    msg: >-
+      NOTE: You've enabled the Etherpad tool but are not using the matrix-nginx-proxy
+      reverse proxy.
+      Please make sure that you're proxying the `{{ matrix_etherpad_public_endpoint }}`
+      URL endpoint to the matrix-etherpad container.
+      You can expose the container's port using the `matrix_etherpad_container_http_host_bind_port` variable.
+  when: "matrix_etherpad_enabled|bool and matrix_nginx_proxy_enabled is not defined"
diff --git a/roles/matrix-etherpad/tasks/main.yml b/roles/matrix-etherpad/tasks/main.yml
new file mode 100644
index 000000000..27548aaf9
--- /dev/null
+++ b/roles/matrix-etherpad/tasks/main.yml
@@ -0,0 +1,21 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: run_setup|bool and matrix_etherpad_enabled|bool
+  tags:
+    - setup-all
+    - setup-etherpad
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: run_setup|bool and not matrix_etherpad_enabled|bool
+  tags:
+    - setup-all
+    - setup-etherpad
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: run_setup|bool and matrix_etherpad_enabled|bool
+  tags:
+    - setup-all
+    - setup-etherpad
diff --git a/roles/matrix-etherpad/tasks/setup_install.yml b/roles/matrix-etherpad/tasks/setup_install.yml
new file mode 100644
index 000000000..a93c28de5
--- /dev/null
+++ b/roles/matrix-etherpad/tasks/setup_install.yml
@@ -0,0 +1,36 @@
+---
+
+- name: Ensure Etherpad base path exists
+  file:
+    path: "{{ matrix_etherpad_base_path }}"
+    state: directory
+    mode: 0770
+    owner: "{{ matrix_etherpad_user_uid }}"
+    group: "{{ matrix_etherpad_user_gid }}"
+
+- name: Ensure Etherpad config installed
+  copy:
+    content: "{{ matrix_etherpad_configuration|to_nice_json }}"
+    dest: "{{ matrix_etherpad_base_path }}/settings.json"
+    mode: 0640
+    owner: "{{ matrix_etherpad_user_uid }}"
+    group: "{{ matrix_etherpad_user_gid }}"
+
+- name: Ensure Etherpad image is pulled
+  docker_image:
+    name: "{{ matrix_etherpad_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_etherpad_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_etherpad_docker_image_force_pull }}"
+
+- name: Ensure matrix-etherpad.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-etherpad.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-etherpad.service"
+    mode: 0644
+  register: matrix_etherpad_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-etherpad.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_etherpad_systemd_service_result.changed|bool"
diff --git a/roles/matrix-etherpad/tasks/setup_uninstall.yml b/roles/matrix-etherpad/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..8f40f420e
--- /dev/null
+++ b/roles/matrix-etherpad/tasks/setup_uninstall.yml
@@ -0,0 +1,35 @@
+---
+
+- name: Check existence of matrix-etherpad service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-etherpad.service"
+  register: matrix_etherpad_service_stat
+
+- name: Ensure matrix-etherpad is stopped
+  service:
+    name: matrix-etherpad
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_etherpad_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-etherpad.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-etherpad.service"
+    state: absent
+  when: "matrix_etherpad_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-etherpad.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_etherpad_service_stat.stat.exists|bool"
+
+- name: Ensure Etherpad base directory doesn't exist
+  file:
+    path: "{{ matrix_etherpad_base_path }}"
+    state: absent
+
+- name: Ensure Etherpad Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_etherpad_docker_image }}"
+    state: absent
diff --git a/roles/matrix-etherpad/tasks/validate_config.yml b/roles/matrix-etherpad/tasks/validate_config.yml
new file mode 100644
index 000000000..c76dc3b5d
--- /dev/null
+++ b/roles/matrix-etherpad/tasks/validate_config.yml
@@ -0,0 +1,11 @@
+- name: Fail if Etherpad is enabled without the Dimension integrations manager
+  fail:
+    msg: >-
+      To integrate Etherpad notes with Matrix rooms you need to set "matrix_dimension_enabled" to true
+  when: "not matrix_dimension_enabled|bool"
+
+- name: Fail if no database is configured for Etherpad
+  fail:
+    msg: >-
+      Etherpad requires a dedicated Postgres database. Please enable the built in one, or configure an external DB by redefining "matrix_etherpad_database_hostname"
+  when: matrix_etherpad_database_hostname == "matrix-postgres" and not matrix_postgres_enabled
diff --git a/roles/matrix-etherpad/templates/settings.json.j2 b/roles/matrix-etherpad/templates/settings.json.j2
new file mode 100644
index 000000000..377bad988
--- /dev/null
+++ b/roles/matrix-etherpad/templates/settings.json.j2
@@ -0,0 +1,105 @@
+{
+  "title": {{ matrix_etherpad_title|to_json }},
+  "favicon": "favicon.ico",
+  "skinName": "colibris",
+  "skinVariants": "super-light-toolbar super-light-editor light-background",
+  "ip": "::",
+  "port": 9001,
+  "showSettingsInAdminPage": true,
+  "dbType": {{ matrix_etherpad_database_engine|to_json }},
+  "dbSettings": {
+    "database": {{ matrix_etherpad_database_name|to_json }},
+    "host":     {{ matrix_etherpad_database_hostname|to_json }},
+    "password": {{ matrix_etherpad_database_password|to_json }},
+    "port":     {{ matrix_etherpad_database_port|to_json }},
+    "user":     {{ matrix_etherpad_database_username|to_json }}
+  },
+  "defaultPadText" : {{ matrix_etherpad_default_pad_text|to_json }},
+  "suppressErrorsInPadText": false,
+  "requireSession": false,
+  "editOnly": false,
+  "minify": true,
+  "maxAge": 21600,
+  "abiword": null,
+  "soffice": null,
+  "tidyHtml": null,
+  "allowUnknownFileEnds": true,
+  "requireAuthentication": false,
+  "requireAuthorization": false,
+  "trustProxy": true,
+  "cookie": {
+    "sameSite": "Lax"
+  },
+  "disableIPlogging": true,
+  "automaticReconnectionTimeout": 0,
+  "scrollWhenFocusLineIsOutOfViewport": {
+    "percentage": {
+      "editionAboveViewport": 0,
+      "editionBelowViewport": 0
+    },
+    "duration": 0,
+    "scrollWhenCaretIsInTheLastLineOfViewport": false,
+    "percentageToScrollWhenUserPressesArrowUp": 0
+  },
+  "socketTransportProtocols" : ["xhr-polling", "jsonp-polling", "htmlfile"],
+  "socketIo": {
+    "maxHttpBufferSize": 10000
+  },
+  "loadTest": false,
+  "importExportRateLimiting": {
+    "windowMs": 90000,
+    "max": 10
+  },
+  "importMaxFileSize": 52428800,
+  "commitRateLimiting": {
+    "duration": 1,
+    "points": 10
+  },
+  "exposeVersion": false,
+  "padOptions": {
+    "noColors": false,
+    "showControls": true,
+    "showChat": false,
+    "showLineNumbers": true,
+    "useMonospaceFont": false,
+    "userName": false,
+    "userColor": false,
+    "rtl": false,
+    "alwaysShowChat": false,
+    "chatAndUsers": false,
+    "lang": "en-gb"
+  },
+  "padShortcutEnabled" : {
+    "altF9": true,
+    "altC": true,
+    "cmdShift2": true,
+    "delete": true,
+    "return": true,
+    "esc": true,
+    "cmdS": true,
+    "tab": true,
+    "cmdZ": true,
+    "cmdY": true,
+    "cmdI": true,
+    "cmdB": true,
+    "cmdU": true,
+    "cmd5": true,
+    "cmdShiftL": true,
+    "cmdShiftN": true,
+    "cmdShift1": true,
+    "cmdShiftC": true,
+    "cmdH": true,
+    "ctrlHome": true,
+    "pageUp": true,
+    "pageDown": true
+  },
+  "loglevel": "INFO",
+  "logconfig" :
+    { "appenders": [
+        { "type": "console",
+          "layout": {"type": "messagePassThrough"}
+        }
+      ]
+    },
+  "customLocaleStrings": {}
+}
diff --git a/roles/matrix-etherpad/templates/systemd/matrix-etherpad.service.j2 b/roles/matrix-etherpad/templates/systemd/matrix-etherpad.service.j2
new file mode 100644
index 000000000..b579036be
--- /dev/null
+++ b/roles/matrix-etherpad/templates/systemd/matrix-etherpad.service.j2
@@ -0,0 +1,44 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Etherpad
+{% for service in matrix_etherpad_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_etherpad_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_docker }} kill matrix-etherpad
+ExecStartPre=-{{ matrix_host_command_docker }} rm matrix-etherpad
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-etherpad \
+			--log-driver=none \
+			--user={{ matrix_etherpad_user_uid }}:{{ matrix_etherpad_user_gid }} \
+			--cap-drop=ALL \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_etherpad_container_http_host_bind_port %}
+			-p {{ matrix_etherpad_container_http_host_bind_port }}:9001 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_etherpad_base_path }},dst=/data \
+			{% for arg in matrix_etherpad_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_etherpad_docker_image }} \
+			node --experimental-worker src/node/server.js \
+				--settings /data/settings.json --credentials /data/credentials.json \
+				--sessionkey /data/sessionkey.json --apikey /data/apijey.json
+				
+
+ExecStop=-{{ matrix_host_command_docker }} kill matrix-etherpad
+ExecStop=-{{ matrix_host_command_docker }} rm matrix-etherpad
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-etherpad
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-grafana/defaults/main.yml b/roles/matrix-grafana/defaults/main.yml
new file mode 100644
index 000000000..88359fe14
--- /dev/null
+++ b/roles/matrix-grafana/defaults/main.yml
@@ -0,0 +1,59 @@
+# matrix-grafana is open source visualization and analytics software
+# See: https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md
+
+matrix_grafana_enabled: false
+
+matrix_grafana_version: 8.0.5
+matrix_grafana_docker_image: "{{ matrix_container_global_registry_prefix }}grafana/grafana:{{ matrix_grafana_version }}"
+matrix_grafana_docker_image_force_pull: "{{ matrix_grafana_docker_image.endswith(':latest') }}"
+
+# Not conditional, because when someone disables metrics
+# they might still want to look at the old existing data.
+# So it would be silly to delete the dashboard in such case.
+matrix_grafana_dashboard_download_urls:
+- "https://raw.githubusercontent.com/matrix-org/synapse/master/contrib/grafana/synapse.json"
+- "https://raw.githubusercontent.com/rfrail3/grafana-dashboards/master/prometheus/node-exporter-full.json"
+
+matrix_grafana_base_path: "{{ matrix_base_data_path }}/grafana"
+matrix_grafana_config_path: "{{ matrix_grafana_base_path }}/config"
+matrix_grafana_data_path: "{{ matrix_grafana_base_path }}/data"
+
+# Allow viewing Grafana without logging in
+matrix_grafana_anonymous_access: false
+
+# specify organization name that should be used for unauthenticated users
+# if you change this in the Grafana admin panel, this needs to be updated
+# to match to keep anonymous logins working
+matrix_grafana_anonymous_access_org_name: 'Main Org.'
+
+
+# default admin credentials, you are asked to change these on first login
+matrix_grafana_default_admin_user: admin
+matrix_grafana_default_admin_password: admin
+
+# Set to true to add the Content-Security-Policy header to your requests.
+# CSP allows to control resources that the user agent can load and helps 
+# prevent XSS attacks.
+# [Content Security Policy](https://grafana.com/docs/grafana/latest/administration/configuration/#content_security_policy)
+matrix_grafana_content_security_policy: true
+
+# specify content security policy template to customized template
+# added https: and http: url schemes (ignored by browsers supporting 'strict-dynamic') to be backward compatible with older browsers.
+# [Content Security Policy Browser Test] (https://content-security-policy.com/browser-test/)
+# [Content Security Policy Reference](https://content-security-policy.com/script-src/)
+matrix_grafana_content_security_policy_customized: false
+matrix_grafana_content_security_policy_template: "script-src 'self' 'unsafe-eval' 'unsafe-inline' http: https: 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline' blob:;img-src * data:;base-uri 'self';connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;manifest-src 'self';media-src 'none';form-action 'self';"
+
+# A list of extra arguments to pass to the container
+matrix_grafana_container_extra_arguments: []
+
+# List of systemd services that matrix-grafana.service depends on
+matrix_grafana_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-grafana.service wants
+matrix_grafana_systemd_wanted_services_list: []
+
+# Controls whether the matrix-grafana container exposes its HTTP port (tcp/3000 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:3000"), or empty string to not expose.
+matrix_grafana_container_http_host_bind_port: ''
diff --git a/roles/matrix-grafana/tasks/init.yml b/roles/matrix-grafana/tasks/init.yml
new file mode 100644
index 000000000..8a22e3018
--- /dev/null
+++ b/roles/matrix-grafana/tasks/init.yml
@@ -0,0 +1,5 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-grafana.service'] }}"
+  when: matrix_grafana_enabled|bool
+
+
diff --git a/roles/matrix-grafana/tasks/main.yml b/roles/matrix-grafana/tasks/main.yml
new file mode 100644
index 000000000..fb16c394b
--- /dev/null
+++ b/roles/matrix-grafana/tasks/main.yml
@@ -0,0 +1,14 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_grafana_enabled|bool"
+  tags:
+    - setup-all
+    - setup-grafana
+
+- import_tasks: "{{ role_path }}/tasks/setup.yml"
+  tags:
+    - setup-all
+    - setup-grafana
diff --git a/roles/matrix-grafana/tasks/setup.yml b/roles/matrix-grafana/tasks/setup.yml
new file mode 100644
index 000000000..00d2e230d
--- /dev/null
+++ b/roles/matrix-grafana/tasks/setup.yml
@@ -0,0 +1,110 @@
+---
+
+#
+# Tasks related to setting up matrix-grafana
+#
+
+- name: Ensure matrix-grafana image is pulled
+  docker_image:
+    name: "{{ matrix_grafana_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_grafana_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_grafana_docker_image_force_pull }}"
+  when: "matrix_grafana_enabled|bool"
+
+- name: Ensure grafana paths exists
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_grafana_base_path }}"
+    - "{{ matrix_grafana_config_path }}"
+    - "{{ matrix_grafana_config_path }}/provisioning"
+    - "{{ matrix_grafana_config_path }}/provisioning/datasources"
+    - "{{ matrix_grafana_config_path }}/provisioning/dashboards"
+    - "{{ matrix_grafana_config_path }}/dashboards"
+    - "{{ matrix_grafana_data_path }}"
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure grafana.ini present
+  template:
+    src: "{{ role_path }}/templates/grafana.ini.j2"
+    dest: "{{ matrix_grafana_config_path }}/grafana.ini"
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure provisioning/datasources/default.yaml present
+  template:
+    src: "{{ role_path }}/templates/datasources.yaml.j2"
+    dest: "{{ matrix_grafana_config_path }}/provisioning/datasources/default.yaml"
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure provisioning/dashboards/default.yaml present
+  template:
+    src: "{{ role_path }}/templates/dashboards.yaml.j2"
+    dest: "{{ matrix_grafana_config_path }}/provisioning/dashboards/default.yaml"
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure dashboard(s) downloaded
+  get_url:
+    url: "{{ item }}"
+    dest: "{{ matrix_grafana_config_path }}/dashboards/"
+    force: true
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items: "{{ matrix_grafana_dashboard_download_urls_all }}"
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure matrix-grafana.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-grafana.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-grafana.service"
+    mode: 0644
+  register: matrix_grafana_systemd_service_result
+  when: matrix_grafana_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-grafana.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_grafana_enabled|bool and matrix_grafana_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of matrix-grafana (if it was previously enabled)
+#
+
+- name: Check existence of matrix-grafana service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-grafana.service"
+  register: matrix_grafana_service_stat
+
+- name: Ensure matrix-grafana is stopped
+  service:
+    name: matrix-grafana
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_grafana_enabled|bool and matrix_grafana_service_stat.stat.exists"
+
+- name: Ensure matrix-grafana.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-grafana.service"
+    state: absent
+  when: "not matrix_grafana_enabled|bool and matrix_grafana_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-grafana.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_grafana_enabled|bool and matrix_grafana_service_stat.stat.exists"
+
diff --git a/roles/matrix-grafana/tasks/validate_config.yml b/roles/matrix-grafana/tasks/validate_config.yml
new file mode 100644
index 000000000..63d4919a3
--- /dev/null
+++ b/roles/matrix-grafana/tasks/validate_config.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Fail if Prometheus not enabled
+  fail:
+    msg: >
+      You need to enable `matrix_prometheus_enabled` to use Prometheus as data source for Grafana.
+  when: "not matrix_prometheus_enabled"
diff --git a/roles/matrix-grafana/templates/dashboards.yaml.j2 b/roles/matrix-grafana/templates/dashboards.yaml.j2
new file mode 100644
index 000000000..aae42ba29
--- /dev/null
+++ b/roles/matrix-grafana/templates/dashboards.yaml.j2
@@ -0,0 +1,9 @@
+apiVersion: 1
+
+providers:
+  - name: {{ matrix_server_fqn_matrix }} - Dashboards
+    folder: '' # The folder where to place the dashboards
+    type: file
+    allowUiUpdates: true
+    options:
+      path: /etc/grafana/dashboards
diff --git a/roles/matrix-grafana/templates/datasources.yaml.j2 b/roles/matrix-grafana/templates/datasources.yaml.j2
new file mode 100644
index 000000000..6ccbe3742
--- /dev/null
+++ b/roles/matrix-grafana/templates/datasources.yaml.j2
@@ -0,0 +1,8 @@
+apiVersion: 1
+
+datasources:
+  - name: {{ matrix_server_fqn_matrix }} - Prometheus
+    type: prometheus
+    # Access mode - proxy (server in the UI) or direct (browser in the UI).
+    access: proxy
+    url: http://matrix-prometheus:9090
diff --git a/roles/matrix-grafana/templates/grafana.ini.j2 b/roles/matrix-grafana/templates/grafana.ini.j2
new file mode 100644
index 000000000..8f4c88f08
--- /dev/null
+++ b/roles/matrix-grafana/templates/grafana.ini.j2
@@ -0,0 +1,31 @@
+[server]
+root_url = "https://{{ matrix_server_fqn_grafana }}"
+
+[security]
+# default admin user, created on startup
+admin_user = "{{ matrix_grafana_default_admin_user }}"
+
+# default admin password, can be changed before first start of grafana, or in profile settings
+admin_password = """{{ matrix_grafana_default_admin_password }}"""
+
+# specify content_security_policy to add the Content-Security-Policy header to your requests
+content_security_policy = "{{ matrix_grafana_content_security_policy }}"
+
+# specify content security policy template to customized template
+{% if matrix_grafana_content_security_policy_customized %}
+content_security_policy_template = """{{ matrix_grafana_content_security_policy_template }}"""
+{% endif %}
+
+[auth.anonymous]
+# enable anonymous access
+enabled = {{ matrix_grafana_anonymous_access }}
+
+# specify organization name that should be used for unauthenticated users
+org_name = "{{ matrix_grafana_anonymous_access_org_name }}"
+
+[dashboards]
+{% if matrix_synapse_metrics_enabled %}
+default_home_dashboard_path = /etc/grafana/dashboards/synapse.json
+{% else %}
+default_home_dashboard_path = /etc/grafana/dashboards/node-exporter-full.json
+{% endif %}
diff --git a/roles/matrix-grafana/templates/systemd/matrix-grafana.service.j2 b/roles/matrix-grafana/templates/systemd/matrix-grafana.service.j2
new file mode 100644
index 000000000..a4f81e357
--- /dev/null
+++ b/roles/matrix-grafana/templates/systemd/matrix-grafana.service.j2
@@ -0,0 +1,43 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-grafana
+{% for service in matrix_grafana_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_grafana_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-grafana 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-grafana 2>/dev/null'
+
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-grafana \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_grafana_container_http_host_bind_port %}
+			-p {{ matrix_grafana_container_http_host_bind_port }}:3000 \
+			{% endif %}
+			-v {{ matrix_grafana_config_path }}:/etc/grafana:z \
+			-v {{ matrix_grafana_data_path }}:/var/lib/grafana:z \
+			{% for arg in matrix_grafana_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_grafana_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-grafana 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-grafana 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-grafana
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-jitsi/defaults/main.yml b/roles/matrix-jitsi/defaults/main.yml
new file mode 100644
index 000000000..87d877065
--- /dev/null
+++ b/roles/matrix-jitsi/defaults/main.yml
@@ -0,0 +1,261 @@
+matrix_jitsi_enabled: true
+
+matrix_jitsi_base_path: "{{ matrix_base_data_path }}/jitsi"
+
+matrix_jitsi_enable_auth: false
+matrix_jitsi_enable_guests: false
+matrix_jitsi_enable_recording: false
+matrix_jitsi_enable_transcriptions: false
+matrix_jitsi_enable_p2p: true
+
+# Authentication type, must be one of internal, jwt or ldap. Currently only
+# internal and ldap are supported by this playbook.
+matrix_jitsi_auth_type: internal
+
+# Configuration options for LDAP authentication. For details see upstream:
+#   https://github.com/jitsi/docker-jitsi-meet#authentication-using-ldap.
+# Defaults are taken from:
+#   https://github.com/jitsi/docker-jitsi-meet/blob/master/prosody/rootfs/defaults/saslauthd.conf
+matrix_jitsi_ldap_url: ""
+matrix_jitsi_ldap_base: ""
+matrix_jitsi_ldap_binddn: ""
+matrix_jitsi_ldap_bindpw: ""
+matrix_jitsi_ldap_filter: "uid=%u"
+matrix_jitsi_ldap_auth_method: "bind"
+matrix_jitsi_ldap_version: "3"
+matrix_jitsi_ldap_use_tls: false
+matrix_jitsi_ldap_tls_ciphers: ""
+matrix_jitsi_ldap_tls_check_peer: false
+matrix_jitsi_ldap_tls_cacert_file: "/etc/ssl/certs/ca-certificates.crt"
+matrix_jitsi_ldap_tls_cacert_dir: "/etc/ssl/certs"
+matrix_jitsi_ldap_start_tls: false
+
+matrix_jitsi_timezone: UTC
+
+matrix_jitsi_xmpp_domain: matrix-jitsi-web
+matrix_jitsi_xmpp_server: matrix-jitsi-prosody
+matrix_jitsi_xmpp_auth_domain: auth.meet.jitsi
+matrix_jitsi_xmpp_bosh_url_base: http://{{ matrix_jitsi_xmpp_server }}:5280
+matrix_jitsi_xmpp_guest_domain: guest.meet.jitsi
+matrix_jitsi_xmpp_muc_domain: muc.meet.jitsi
+matrix_jitsi_xmpp_internal_muc_domain: internal-muc.meet.jitsi
+matrix_jitsi_xmpp_modules: ''
+
+matrix_jitsi_recorder_domain: recorder.meet.jitsi
+
+
+matrix_jitsi_jibri_brewery_muc: jibribrewery
+matrix_jitsi_jibri_pending_timeout: 90
+matrix_jitsi_jibri_xmpp_user: jibri
+matrix_jitsi_jibri_xmpp_password: ''
+matrix_jitsi_jibri_recorder_user: recorder
+matrix_jitsi_jibri_recorder_password: ''
+
+matrix_jitsi_enable_lobby: false
+
+matrix_jitsi_version: stable-5765-1
+matrix_jitsi_container_image_tag: "{{ matrix_jitsi_version }}" # for backward-compatibility
+
+matrix_jitsi_web_docker_image: "{{ matrix_container_global_registry_prefix }}jitsi/web:{{ matrix_jitsi_container_image_tag }}"
+matrix_jitsi_web_docker_image_force_pull: "{{ matrix_jitsi_web_docker_image.endswith(':latest') }}"
+
+matrix_jitsi_web_base_path: "{{ matrix_base_data_path }}/jitsi/web"
+matrix_jitsi_web_config_path: "{{ matrix_jitsi_web_base_path }}/config"
+matrix_jitsi_web_transcripts_path: "{{ matrix_jitsi_web_base_path }}/transcripts"
+
+matrix_jitsi_web_public_url: "https://{{ matrix_server_fqn_jitsi }}"
+
+# STUN servers used in the web UI. Feel free to point them to your own STUN server.
+# Addresses need to be prefixed with one of `stun:`, `turn:` or `turns:`.
+matrix_jitsi_web_stun_servers: ['stun:meet-jit-si-turnrelay.jitsi.net:443']
+
+# Controls whether Etherpad will be available within Jitsi
+matrix_jitsi_etherpad_enabled: false
+
+# Controls whether the matrix-jitsi-web container exposes its HTTP port (tcp/80 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:13080"), or empty string to not expose.
+matrix_jitsi_web_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_jitsi_web_container_extra_arguments: []
+
+# List of systemd services that matrix-jitsi-web.service depends on
+matrix_jitsi_web_systemd_required_services_list: ['docker.service']
+
+
+# Some variables controlling the interface of Jitsi Web.
+# These get applied to `templates/web/interface_config.js.j2`.
+#
+# Besides this, you can also use `matrix_jitsi_web_custom_interface_config_extension`
+# to define any other configuration option.
+matrix_jitsi_web_interface_config_lang_detection: false
+matrix_jitsi_web_interface_config_show_jitsi_watermark: true
+matrix_jitsi_web_interface_config_jitsi_watermark_link: "https://jitsi.org"
+matrix_jitsi_web_interface_config_show_brand_watermark: false
+matrix_jitsi_web_interface_config_brand_watermark_link: ""
+matrix_jitsi_web_interface_config_generate_room_names_on_welcome_page: true
+matrix_jitsi_web_interface_config_display_welcome_page_content: true
+matrix_jitsi_web_interface_config_app_name: "Jitsi Meet"
+matrix_jitsi_web_interface_config_native_app_name: "Jitsi Meet"
+matrix_jitsi_web_interface_config_provider_name: "Jitsi"
+matrix_jitsi_web_interface_config_show_powered_by: false
+matrix_jitsi_web_interface_config_disable_transcription_subtitles: false
+matrix_jitsi_web_interface_config_show_deep_linking_image: false
+
+# Custom configuration to be injected into `interface_config.js`, passed to Jitsi Web.
+# This configuration gets appended to the final interface configuration that Jitsi Web uses.
+#
+# Note: not to be confused with `matrix_jitsi_web_custom_config_extension`.
+#
+# For interface configuration, the flow is like this:
+# - the contents of `templates/web/interface_config.js.j2` is generated (based on various `matrix_jitsi_web_interface_config_*` variables you see in this file)
+# - the contents of `matrix_jitsi_web_custom_interface_config_extension` is appended and can define new settings or override defaults.
+#
+# Example:
+# matrix_jitsi_web_custom_interface_config_extension: |
+#   interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED = false;
+#   interfaceConfig.DISABLE_VIDEO_BACKGROUND = true;
+matrix_jitsi_web_custom_interface_config_extension: ''
+
+
+# Controls after which participant audio will be muted. If not specified, defaults to Jitsi's default value (likely 10)
+matrix_jitsi_web_config_start_audio_muted_after_nth_participant: ~
+# Controls after which participant video will be muted. If not specified, defaults to Jitsi's default value (likely 10)
+matrix_jitsi_web_config_start_video_muted_after_nth_participant: ~
+
+matrix_jitsi_web_config_defaultLanguage: 'en'
+
+# Ideal and also maximum resolution width. If not specified, defaults to Jitsi's default value (likely 1280)
+matrix_jitsi_web_config_resolution_width_ideal_and_max: ~
+# Minimum resolution width. If not specified, defaults to Jitsi's default value (likely 320)
+matrix_jitsi_web_config_resolution_width_min: ~
+# Ideal and also maximum resolution height. If not specified, defaults to Jitsi's default value (likely 720)
+matrix_jitsi_web_config_resolution_height_ideal_and_max: ~
+# Minimum resolution height. If not specified, defaults to Jitsi's default value (likely 180)
+matrix_jitsi_web_config_resolution_height_min: ~
+
+# Custom configuration to be injected into `custom-config.js`, passed to Jitsi Web.
+# This configuration gets appended to the final configuration that Jitsi Web uses.
+#
+# Note: not to be confused with `matrix_jitsi_web_custom_interface_config_extension`.
+#
+# The flow is like this:
+# - some default configuration is automatically generated based on the environment variables passed to the Jitsi Web container
+# - the contents of `custom-config.js` is appended to it (see `templates/web/custom-config.js.j2`)
+# - said `custom-config.js` contains your custom contents specified in `matrix_jitsi_web_custom_config_extension`.
+#
+# Example:
+# matrix_jitsi_web_custom_config_extension: |
+#   if (!config.hasOwnProperty('testing')) config.testing = {};
+#   config.testing.p2pTestMode = true
+matrix_jitsi_web_custom_config_extension: ''
+
+# Additional environment variables to pass to the Jitsi Web container.
+# You can use this to further influence the default configuration generated by the Jitsi Web container on every startup.
+# Besides influencing the final configuration by passing environment variables, you can also inject custom configuration
+# by using `matrix_jitsi_web_custom_config_extension`.
+#
+# Example:
+# matrix_jitsi_web_environment_variables_extension: |
+#   ENABLE_FILE_RECORDING_SERVICE=1
+#   DROPBOX_APPKEY=something
+#   DROPBOX_REDIRECT_URI=something
+matrix_jitsi_web_environment_variables_extension: ''
+
+
+matrix_jitsi_prosody_docker_image: "{{ matrix_container_global_registry_prefix }}jitsi/prosody:{{ matrix_jitsi_container_image_tag }}"
+matrix_jitsi_prosody_docker_image_force_pull: "{{ matrix_jitsi_prosody_docker_image.endswith(':latest') }}"
+
+matrix_jitsi_prosody_base_path: "{{ matrix_base_data_path }}/jitsi/prosody"
+matrix_jitsi_prosody_config_path: "{{ matrix_jitsi_prosody_base_path }}/config"
+matrix_jitsi_prosody_plugins_path: "{{ matrix_jitsi_prosody_base_path }}/prosody-plugins-custom"
+
+# A list of extra arguments to pass to the container
+matrix_jitsi_prosody_container_extra_arguments: []
+
+# List of systemd services that matrix-jitsi-prosody.service depends on
+matrix_jitsi_prosody_systemd_required_services_list: ['docker.service']
+
+# Neccessary Port binding for those disabling the integrated nginx proxy
+matrix_jitsi_prosody_container_http_host_bind_port: ''
+
+matrix_jitsi_jicofo_docker_image: "{{ matrix_container_global_registry_prefix }}jitsi/jicofo:{{ matrix_jitsi_container_image_tag }}"
+matrix_jitsi_jicofo_docker_image_force_pull: "{{ matrix_jitsi_jicofo_docker_image.endswith(':latest') }}"
+
+matrix_jitsi_jicofo_base_path: "{{ matrix_base_data_path }}/jitsi/jicofo"
+matrix_jitsi_jicofo_config_path: "{{ matrix_jitsi_jicofo_base_path }}/config"
+
+# A list of extra arguments to pass to the container
+matrix_jitsi_jicofo_container_extra_arguments: []
+
+# List of systemd services that matrix-jitsi-jicofo.service depends on
+matrix_jitsi_jicofo_systemd_required_services_list: ['docker.service', 'matrix-jitsi-prosody.service']
+
+matrix_jitsi_jicofo_component_secret: ''
+matrix_jitsi_jicofo_auth_user: focus
+matrix_jitsi_jicofo_auth_password: ''
+
+
+matrix_jitsi_jvb_docker_image: "{{ matrix_container_global_registry_prefix }}jitsi/jvb:{{ matrix_jitsi_container_image_tag }}"
+matrix_jitsi_jvb_docker_image_force_pull: "{{ matrix_jitsi_jvb_docker_image.endswith(':latest') }}"
+
+matrix_jitsi_jvb_base_path: "{{ matrix_base_data_path }}/jitsi/jvb"
+matrix_jitsi_jvb_config_path: "{{ matrix_jitsi_jvb_base_path }}/config"
+
+# A list of extra arguments to pass to the container
+matrix_jitsi_jvb_container_extra_arguments: []
+
+# List of systemd services that matrix-jitsi-jvb.service depends on
+matrix_jitsi_jvb_systemd_required_services_list: ['docker.service', 'matrix-jitsi-prosody.service']
+
+matrix_jitsi_jvb_auth_user: jvb
+matrix_jitsi_jvb_auth_password: ''
+
+# STUN servers used by JVB on the server-side, so it can discover its own external IP address.
+# Pointing this to a STUN server running on the same Docker network may lead to incorrect IP address discovery.
+matrix_jitsi_jvb_stun_servers: ['meet-jit-si-turnrelay.jitsi.net:443']
+
+matrix_jitsi_jvb_brewery_muc: jvbbrewery
+matrix_jitsi_jvb_rtp_udp_port: 10000
+matrix_jitsi_jvb_rtp_tcp_port: 4443
+
+# Custom configuration to be injected into `custom-sip-communicator.properties`, passed to Jitsi JVB.
+# This configuration gets appended to the final configuration that Jitsi JVB uses.
+#
+# The flow is like this:
+# - some default configuration is automatically generated based on the environment variables passed to the Jitsi JVB container
+# - the contents of `custom-sip-communicator.properties` is appended to it (see `templates/jvb/custom-sip-communicator.properties.j2`)
+# - said `custom-sip-communicator.properties` contains your custom contents specified in `matrix_jitsi_jvb_custom_config_extension`.
+#
+# Example:
+# matrix_jitsi_jvb_custom_config_extension: |
+#   org.jitsi.videobridge.xmpp.user.shard.DISABLE_CERTIFICATE_VERIFICATION=false
+#   org.jitsi.videobridge.ENABLE_STATISTICS=false
+matrix_jitsi_jvb_custom_config_extension: ''
+
+# Additional environment variables to pass to the Jitsi JVB container.
+# You can use this to further influence the default configuration generated by the Jitsi JVB container on every startup.
+# Besides influencing the final configuration by passing environment variables, you can also inject custom configuration
+# by using `matrix_jitsi_jvb_custom_config_extension`.
+#
+# Example:
+# matrix_jitsi_jvb_environment_variables_extension: |
+#   SOME_VARIABLE=1
+#   ANOTHER_VARIABLE=something
+matrix_jitsi_jvb_environment_variables_extension: ''
+
+# Controls whether the matrix-jitsi-jvb container exposes its RTP UDP port (udp/10000 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:10000"), or empty string to not expose.
+matrix_jitsi_jvb_container_rtp_udp_host_bind_port: "{{ matrix_jitsi_jvb_rtp_udp_port }}"
+
+# Controls whether the matrix-jitsi-jvb container exposes its RTP UDP port (udp/4443 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:4443"), or empty string to not expose.
+matrix_jitsi_jvb_container_rtp_tcp_host_bind_port: "{{ matrix_jitsi_jvb_rtp_tcp_port }}"
+
+# Controls whether the matrix-jitsi-jvb container exposes its Colibri WebSocket port (tcp/9090 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:12090"), or empty string to not expose.
+matrix_jitsi_jvb_container_colibri_ws_host_bind_port: ''
diff --git a/roles/matrix-jitsi/tasks/init.yml b/roles/matrix-jitsi/tasks/init.yml
new file mode 100644
index 000000000..1f7a2d1cf
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-jitsi-web.service', 'matrix-jitsi-prosody.service', 'matrix-jitsi-jicofo.service', 'matrix-jitsi-jvb.service'] }}"
+  when: matrix_jitsi_enabled|bool
diff --git a/roles/matrix-jitsi/tasks/main.yml b/roles/matrix-jitsi/tasks/main.yml
new file mode 100644
index 000000000..e4f3508f3
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/main.yml
@@ -0,0 +1,39 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_jitsi_enabled|bool"
+  tags:
+    - setup-all
+    - setup-jitsi
+
+- import_tasks: "{{ role_path }}/tasks/setup_jitsi_base.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-jitsi
+
+- import_tasks: "{{ role_path }}/tasks/setup_jitsi_web.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-jitsi
+
+- import_tasks: "{{ role_path }}/tasks/setup_jitsi_prosody.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-jitsi
+
+- import_tasks: "{{ role_path }}/tasks/setup_jitsi_jicofo.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-jitsi
+
+- import_tasks: "{{ role_path }}/tasks/setup_jitsi_jvb.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-jitsi
diff --git a/roles/matrix-jitsi/tasks/setup_jitsi_base.yml b/roles/matrix-jitsi/tasks/setup_jitsi_base.yml
new file mode 100644
index 000000000..408027ee0
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/setup_jitsi_base.yml
@@ -0,0 +1,20 @@
+---
+
+#
+# Tasks related to setting up jitsi
+#
+
+- name: Ensure Matrix jitsi base path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_jitsi_base_path }}", when: true }
+  when: matrix_jitsi_enabled|bool and item.when
+
+#
+# Tasks related to getting rid of jitsi (if it was previously enabled)
+#
diff --git a/roles/matrix-jitsi/tasks/setup_jitsi_jicofo.yml b/roles/matrix-jitsi/tasks/setup_jitsi_jicofo.yml
new file mode 100644
index 000000000..dd2a7bd24
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/setup_jitsi_jicofo.yml
@@ -0,0 +1,93 @@
+---
+
+#
+# Tasks related to setting up jitsi-jicofo
+#
+
+- name: Ensure Matrix jitsi-jicofo path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0777
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_jitsi_jicofo_base_path }}", when: true }
+    - { path: "{{ matrix_jitsi_jicofo_config_path }}", when: true }
+  when: matrix_jitsi_enabled|bool and item.when
+
+- name: Ensure jitsi-jicofo Docker image is pulled
+  docker_image:
+    name: "{{ matrix_jitsi_jicofo_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_jitsi_jicofo_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_jitsi_jicofo_docker_image_force_pull }}"
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-jicofo environment variables file created
+  template:
+    src: "{{ role_path }}/templates/jicofo/env.j2"
+    dest: "{{ matrix_jitsi_jicofo_base_path }}/env"
+    mode: 0640
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-jicofo configuration files created
+  template:
+    src: "{{ role_path }}/templates/jicofo/{{ item }}.j2"
+    dest: "{{ matrix_jitsi_jicofo_config_path }}/{{ item }}"
+    mode: 0644
+  with_items:
+    - sip-communicator.properties
+    - logging.properties
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure matrix-jitsi-jicofo.service installed
+  template:
+    src: "{{ role_path }}/templates/jicofo/matrix-jitsi-jicofo.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-jitsi-jicofo.service"
+    mode: 0644
+  register: matrix_jitsi_jicofo_systemd_service_result
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-jitsi-jicofo.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_jitsi_enabled and matrix_jitsi_jicofo_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of jitsi-jicofo (if it was previously enabled)
+#
+
+- name: Check existence of matrix-jitsi-jicofo service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-jicofo.service"
+  register: matrix_jitsi_jicofo_service_stat
+  when: "not matrix_jitsi_enabled|bool"
+
+- name: Ensure matrix-jitsi-jicofo is stopped
+  service:
+    name: matrix-jitsi-jicofo
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jicofo_service_stat.stat.exists"
+
+- name: Ensure matrix-jitsi-jicofo.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-jicofo.service"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jicofo_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-jitsi-jicofo.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jicofo_service_stat.stat.exists"
+
+- name: Ensure Matrix jitsi-jicofo paths doesn't exist
+  file:
+    path: "{{ matrix_jitsi_jicofo_base_path }}"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool"
+
+# Intentionally not removing the Docker image when uninstalling.
+# We can't be sure it had been pulled by us in the first place.
diff --git a/roles/matrix-jitsi/tasks/setup_jitsi_jvb.yml b/roles/matrix-jitsi/tasks/setup_jitsi_jvb.yml
new file mode 100644
index 000000000..b73426db7
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/setup_jitsi_jvb.yml
@@ -0,0 +1,93 @@
+---
+
+#
+# Tasks related to setting up jitsi-jvb
+#
+
+- name: Ensure Matrix jitsi-jvb path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0777
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_jitsi_jvb_base_path }}", when: true }
+    - { path: "{{ matrix_jitsi_jvb_config_path }}", when: true }
+  when: matrix_jitsi_enabled|bool and item.when
+
+- name: Ensure jitsi-jvb Docker image is pulled
+  docker_image:
+    name: "{{ matrix_jitsi_jvb_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_jitsi_jvb_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_jitsi_jvb_docker_image_force_pull }}"
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-jvb configuration files created
+  template:
+    src: "{{ role_path }}/templates/jvb/{{ item }}.j2"
+    dest: "{{ matrix_jitsi_jvb_config_path }}/{{ item }}"
+    mode: 0644
+  with_items:
+    - custom-sip-communicator.properties
+    - logging.properties
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-jvb environment variables file created
+  template:
+    src: "{{ role_path }}/templates/jvb/env.j2"
+    dest: "{{ matrix_jitsi_jvb_base_path }}/env"
+    mode: 0640
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure matrix-jitsi-jvb.service installed
+  template:
+    src: "{{ role_path }}/templates/jvb/matrix-jitsi-jvb.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-jitsi-jvb.service"
+    mode: 0644
+  register: matrix_jitsi_jvb_systemd_service_result
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-jitsi-jvb.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_jitsi_enabled and matrix_jitsi_jvb_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of jitsi-jvb (if it was previously enabled)
+#
+
+- name: Check existence of matrix-jitsi-jvb service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-jvb.service"
+  register: matrix_jitsi_jvb_service_stat
+  when: "not matrix_jitsi_enabled|bool"
+
+- name: Ensure matrix-jitsi-jvb is stopped
+  service:
+    name: matrix-jitsi-jvb
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jvb_service_stat.stat.exists"
+
+- name: Ensure matrix-jitsi-jvb.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-jvb.service"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jvb_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-jitsi-jvb.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_jvb_service_stat.stat.exists"
+
+- name: Ensure Matrix jitsi-jvb paths doesn't exist
+  file:
+    path: "{{ matrix_jitsi_jvb_base_path }}"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool"
+
+# Intentionally not removing the Docker image when uninstalling.
+# We can't be sure it had been pulled by us in the first place.
diff --git a/roles/matrix-jitsi/tasks/setup_jitsi_prosody.yml b/roles/matrix-jitsi/tasks/setup_jitsi_prosody.yml
new file mode 100644
index 000000000..fd051fdad
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/setup_jitsi_prosody.yml
@@ -0,0 +1,84 @@
+---
+
+#
+# Tasks related to setting up jitsi-prosody
+#
+
+- name: Ensure Matrix jitsi-prosody path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0777
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_jitsi_prosody_base_path }}", when: true }
+    - { path: "{{ matrix_jitsi_prosody_config_path }}", when: true }
+    - { path: "{{ matrix_jitsi_prosody_plugins_path }}", when: true }
+  when: matrix_jitsi_enabled|bool and item.when
+
+- name: Ensure jitsi-prosody Docker image is pulled
+  docker_image:
+    name: "{{ matrix_jitsi_prosody_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_jitsi_prosody_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_jitsi_prosody_docker_image_force_pull }}"
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-prosody environment variables file created
+  template:
+    src: "{{ role_path }}/templates/prosody/env.j2"
+    dest: "{{ matrix_jitsi_prosody_base_path }}/env"
+    mode: 0640
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure matrix-jitsi-prosody.service installed
+  template:
+    src: "{{ role_path }}/templates/prosody/matrix-jitsi-prosody.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-jitsi-prosody.service"
+    mode: 0644
+  register: matrix_jitsi_prosody_systemd_service_result
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-jitsi-prosody.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_jitsi_enabled and matrix_jitsi_prosody_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of jitsi-prosody (if it was previously enabled)
+#
+
+- name: Check existence of matrix-jitsi-prosody service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-prosody.service"
+  register: matrix_jitsi_prosody_service_stat
+  when: "not matrix_jitsi_enabled|bool"
+
+- name: Ensure matrix-jitsi-prosody is stopped
+  service:
+    name: matrix-jitsi-prosody
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_prosody_service_stat.stat.exists"
+
+- name: Ensure matrix-jitsi-prosody.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-prosody.service"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_prosody_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-jitsi-prosody.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_prosody_service_stat.stat.exists"
+
+- name: Ensure Matrix jitsi-prosody paths doesn't exist
+  file:
+    path: "{{ matrix_jitsi_prosody_base_path }}"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool"
+
+# Intentionally not removing the Docker image when uninstalling.
+# We can't be sure it had been pulled by us in the first place.
diff --git a/roles/matrix-jitsi/tasks/setup_jitsi_web.yml b/roles/matrix-jitsi/tasks/setup_jitsi_web.yml
new file mode 100644
index 000000000..2b8a2cd2b
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/setup_jitsi_web.yml
@@ -0,0 +1,95 @@
+---
+
+#
+# Tasks related to setting up jitsi-web
+#
+
+- name: Ensure Matrix jitsi-web path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0777
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_jitsi_web_base_path }}", when: true }
+    - { path: "{{ matrix_jitsi_web_config_path }}", when: true }
+    - { path: "{{ matrix_jitsi_web_transcripts_path }}", when: true }
+  when: matrix_jitsi_enabled|bool and item.when
+
+- name: Ensure jitsi-web Docker image is pulled
+  docker_image:
+    name: "{{ matrix_jitsi_web_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_jitsi_web_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_jitsi_web_docker_image_force_pull }}"
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-web environment variables file created
+  template:
+    src: "{{ role_path }}/templates/web/env.j2"
+    dest: "{{ matrix_jitsi_web_base_path }}/env"
+    mode: 0640
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure jitsi-web configuration files created
+  template:
+    src: "{{ role_path }}/templates/web/{{ item }}.j2"
+    dest: "{{ matrix_jitsi_web_config_path }}/{{ item }}"
+    mode: 0644
+  with_items:
+    - custom-config.js
+    - interface_config.js
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure matrix-jitsi-web.service installed
+  template:
+    src: "{{ role_path }}/templates/web/matrix-jitsi-web.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-jitsi-web.service"
+    mode: 0644
+  register: matrix_jitsi_web_systemd_service_result
+  when: matrix_jitsi_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-jitsi-web.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_jitsi_enabled and matrix_jitsi_web_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of jitsi-web (if it was previously enabled)
+#
+
+- name: Check existence of matrix-jitsi-web service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-web.service"
+  register: matrix_jitsi_web_service_stat
+  when: "not matrix_jitsi_enabled|bool"
+
+- name: Ensure matrix-jitsi-web is stopped
+  service:
+    name: matrix-jitsi-web
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_web_service_stat.stat.exists"
+
+- name: Ensure matrix-jitsi-web.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-jitsi-web.service"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_web_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-jitsi-web.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_jitsi_enabled|bool and matrix_jitsi_web_service_stat.stat.exists"
+
+- name: Ensure Matrix jitsi-web paths doesn't exist
+  file:
+    path: "{{ matrix_jitsi_web_base_path }}"
+    state: absent
+  when: "not matrix_jitsi_enabled|bool"
+
+# Intentionally not removing the Docker image when uninstalling.
+# We can't be sure it had been pulled by us in the first place.
+
diff --git a/roles/matrix-jitsi/tasks/validate_config.yml b/roles/matrix-jitsi/tasks/validate_config.yml
new file mode 100644
index 000000000..cc8a4b224
--- /dev/null
+++ b/roles/matrix-jitsi/tasks/validate_config.yml
@@ -0,0 +1,43 @@
+---
+
+- name: Fail if required Jitsi settings not defined
+  fail:
+    msg: >-
+      You need to define a required configuration setting (`{{ item }}`) for using Jitsi.
+
+      If you're setting up Jitsi for the first time, you may have missed a step.
+      Refer to our setup instructions (docs/configuring-playbook-jitsi.md).
+
+      If you had setup Jitsi successfully before and it's just now that you're observing this failure,
+      it means that your installation may be using some default passwords that the playbook used to define until now.
+      This is not secure and we urge you to rebuild your Jitsi setup.
+      Refer to the "Rebuilding your Jitsi installation" section in our setup instructions (docs/configuring-playbook-jitsi.md).
+  when: "vars[item] == ''"
+  with_items:
+    - "matrix_jitsi_jibri_xmpp_password"
+    - "matrix_jitsi_jibri_recorder_password"
+    - "matrix_jitsi_jicofo_component_secret"
+    - "matrix_jitsi_jicofo_auth_password"
+    - "matrix_jitsi_jvb_auth_password"
+
+- name: (Deprecation) Catch and report renamed settings
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in vars"
+  with_items:
+    - {'old': 'matrix_jitsi_web_config_constraints_enabled', 'new': '<Now unnecessary. Constraints are always applied automatically>'}
+    - {'old': 'matrix_jitsi_web_config_constraints_video_aspectRatio', 'new': '<Not applicable anymore>'}
+    - {'old': 'matrix_jitsi_web_config_constraints_video_height_ideal', 'new': 'matrix_jitsi_web_config_resolution_height_ideal_and_max'}
+    - {'old': 'matrix_jitsi_web_config_constraints_video_height_max', 'new': 'matrix_jitsi_web_config_resolution_height_ideal_and_max'}
+    - {'old': 'matrix_jitsi_web_config_constraints_video_height_min', 'new': 'matrix_jitsi_web_config_resolution_height_min'}
+    - {'old': 'matrix_jitsi_web_config_disableAudioLevels', 'new': '<Can be set by using matrix_jitsi_web_custom_config_extension. Example in docs/configuring-playbook-jitsi.md>'}
+    - {'old': 'matrix_jitsi_web_config_enableLayerSuspension', 'new': '<Can be set by using matrix_jitsi_web_custom_config_extension. Example in docs/configuring-playbook-jitsi.md>'}
+    - {'old': 'matrix_jitsi_web_config_channelLastN', 'new': '<Can be set by using matrix_jitsi_web_custom_config_extension. Example in docs/configuring-playbook-jitsi.md>'}
+    - {'old': 'matrix_jitsi_web_config_testing_p2pTestMode', 'new': '<Can be set by using matrix_jitsi_web_custom_config_extension>'}
+    - {'old': 'matrix_jitsi_web_config_start_with_audio_muted', 'new': '<Superseded by matrix_jitsi_web_config_start_audio_muted_after_nth_participant>'}
+    - {'old': 'matrix_jitsi_web_config_start_with_video_muted', 'new': '<Superseded by matrix_jitsi_web_config_start_video_muted_after_nth_participant>'}
+    - {'old': 'matrix_jitsi_web_interface_config_show_watermark_for_guests', 'new': '<Not applicable anymore>'}
+    - {'old': 'matrix_jitsi_web_interface_config_invitation_powered_by', 'new': '<Not applicable anymore>'}
+    - {'old': 'matrix_jisti_web_interface_config_show_deep_linking_image', 'new': 'matrix_jitsi_web_interface_config_show_deep_linking_image'}
diff --git a/roles/matrix-jitsi/templates/jicofo/env.j2 b/roles/matrix-jitsi/templates/jicofo/env.j2
new file mode 100644
index 000000000..a402d2d75
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jicofo/env.j2
@@ -0,0 +1,17 @@
+ENABLE_AUTH={{ 1 if matrix_jitsi_enable_auth else 0 }}
+
+XMPP_DOMAIN={{ matrix_jitsi_xmpp_domain }}
+XMPP_AUTH_DOMAIN={{ matrix_jitsi_xmpp_auth_domain }}
+XMPP_INTERNAL_MUC_DOMAIN={{ matrix_jitsi_xmpp_internal_muc_domain }}
+XMPP_SERVER={{ matrix_jitsi_xmpp_server }}
+
+JICOFO_COMPONENT_SECRET={{ matrix_jitsi_jicofo_component_secret }}
+JICOFO_AUTH_USER={{ matrix_jitsi_jicofo_auth_user }}
+JICOFO_AUTH_PASSWORD={{ matrix_jitsi_jicofo_auth_password }}
+
+JVB_BREWERY_MUC={{ matrix_jitsi_jvb_brewery_muc }}
+
+JIBRI_BREWERY_MUC={{ matrix_jitsi_jibri_brewery_muc }}
+JIBRI_PENDING_TIMEOUT={{ matrix_jitsi_jibri_pending_timeout }}
+
+TZ={{ matrix_jitsi_timezone }}
diff --git a/roles/matrix-jitsi/templates/jicofo/logging.properties.j2 b/roles/matrix-jitsi/templates/jicofo/logging.properties.j2
new file mode 100644
index 000000000..7eba95af6
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jicofo/logging.properties.j2
@@ -0,0 +1,20 @@
+handlers= java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+
+net.java.sip.communicator.util.ScLogFormatter.programname=Jicofo
+
+.level=INFO
+net.sf.level=SEVERE
+net.java.sip.communicator.plugin.reconnectplugin.level=FINE
+org.ice4j.level=SEVERE
+org.jitsi.impl.neomedia.level=SEVERE
+
+# Do not worry about missing strings
+net.java.sip.communicator.service.resources.AbstractResourcesService.level=SEVERE
+
+#net.java.sip.communicator.service.protocol.level=ALL
+
+# Enable debug packets logging
+#org.jitsi.impl.protocol.xmpp.level=FINE
diff --git a/roles/matrix-jitsi/templates/jicofo/matrix-jitsi-jicofo.service.j2 b/roles/matrix-jitsi/templates/jicofo/matrix-jitsi-jicofo.service.j2
new file mode 100644
index 000000000..6ecafaa03
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jicofo/matrix-jitsi-jicofo.service.j2
@@ -0,0 +1,33 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix jitsi-jicofo server
+{% for service in matrix_jitsi_jicofo_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-jicofo 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-jicofo 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-jitsi-jicofo \
+			--log-driver=none \
+			--network={{ matrix_docker_network }} \
+			--env-file={{ matrix_jitsi_jicofo_base_path }}/env \
+			--mount type=bind,src={{ matrix_jitsi_jicofo_config_path }},dst=/config \
+			{% for arg in matrix_jitsi_jicofo_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_jitsi_jicofo_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-jicofo 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-jicofo 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-jitsi-jicofo
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-jitsi/templates/jicofo/sip-communicator.properties.j2 b/roles/matrix-jitsi/templates/jicofo/sip-communicator.properties.j2
new file mode 100644
index 000000000..c62e04ffe
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jicofo/sip-communicator.properties.j2
@@ -0,0 +1,9 @@
+org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED=true
+org.jitsi.jicofo.BRIDGE_MUC={{ matrix_jitsi_jvb_brewery_muc }}@{{ matrix_jitsi_xmpp_internal_muc_domain }}
+
+org.jitsi.jicofo.jibri.BREWERY={{ matrix_jitsi_jibri_brewery_muc }}@{{ matrix_jitsi_xmpp_internal_muc_domain }}
+org.jitsi.jicofo.jibri.PENDING_TIMEOUT=90
+
+{% if matrix_jitsi_enable_auth %}
+org.jitsi.jicofo.auth.URL=XMPP:{{ matrix_jitsi_xmpp_domain }}
+{% endif %}
diff --git a/roles/matrix-jitsi/templates/jvb/custom-sip-communicator.properties.j2 b/roles/matrix-jitsi/templates/jvb/custom-sip-communicator.properties.j2
new file mode 100644
index 000000000..44b6b8c2c
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jvb/custom-sip-communicator.properties.j2
@@ -0,0 +1,7 @@
+org.jitsi.videobridge.xmpp.user.shard.DISABLE_CERTIFICATE_VERIFICATION=true
+
+org.jitsi.videobridge.ENABLE_STATISTICS=true
+org.jitsi.videobridge.STATISTICS_TRANSPORT=muc
+org.jitsi.videobridge.STATISTICS_INTERVAL=5000
+
+{{ matrix_jitsi_jvb_custom_config_extension }}
diff --git a/roles/matrix-jitsi/templates/jvb/env.j2 b/roles/matrix-jitsi/templates/jvb/env.j2
new file mode 100644
index 000000000..f7dc9247a
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jvb/env.j2
@@ -0,0 +1,20 @@
+JVB_AUTH_PASSWORD={{ matrix_jitsi_jvb_auth_password }}
+JVB_TCP_PORT={{ matrix_jitsi_jvb_rtp_tcp_port }}
+JVB_PORT={{ matrix_jitsi_jvb_rtp_udp_port }}
+JVB_AUTH_USER={{ matrix_jitsi_jvb_auth_user }}
+JVB_AUTH_PASSWORD={{ matrix_jitsi_jvb_auth_password }}
+JVB_BREWERY_MUC={{ matrix_jitsi_jvb_brewery_muc }}
+
+XMPP_SERVER={{ matrix_jitsi_xmpp_server }}
+XMPP_AUTH_DOMAIN={{ matrix_jitsi_xmpp_auth_domain }}
+XMPP_INTERNAL_MUC_DOMAIN={{ matrix_jitsi_xmpp_internal_muc_domain }}
+
+HOSTNAME=matrix-jitsi-jvb
+
+{% if matrix_jitsi_jvb_stun_servers|length > 0 %}
+JVB_STUN_SERVERS={{ matrix_jitsi_jvb_stun_servers|join(',') }}
+{% endif %}
+
+PUBLIC_URL={{ matrix_jitsi_web_public_url }}
+
+{{ matrix_jitsi_jvb_environment_variables_extension }}
diff --git a/roles/matrix-jitsi/templates/jvb/logging.properties.j2 b/roles/matrix-jitsi/templates/jvb/logging.properties.j2
new file mode 100644
index 000000000..48c1e9fa5
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jvb/logging.properties.j2
@@ -0,0 +1,13 @@
+handlers= java.util.logging.ConsoleHandler
+
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
+
+net.java.sip.communicator.util.ScLogFormatter.programname=JVB
+
+.level=INFO
+
+org.jitsi.videobridge.xmpp.ComponentImpl.level=FINE
+
+# All of the INFO level logs from MediaStreamImpl are unnecessary in the context of jitsi-videobridge.
+org.jitsi.impl.neomedia.MediaStreamImpl.level=WARNING
diff --git a/roles/matrix-jitsi/templates/jvb/matrix-jitsi-jvb.service.j2 b/roles/matrix-jitsi/templates/jvb/matrix-jitsi-jvb.service.j2
new file mode 100644
index 000000000..53c0c83ac
--- /dev/null
+++ b/roles/matrix-jitsi/templates/jvb/matrix-jitsi-jvb.service.j2
@@ -0,0 +1,42 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix jitsi-jvb server
+{% for service in matrix_jitsi_jvb_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-jvb 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-jvb 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-jitsi-jvb \
+			--log-driver=none \
+			--network={{ matrix_docker_network }} \
+			--env-file={{ matrix_jitsi_jvb_base_path }}/env \
+			{% if matrix_jitsi_jvb_container_rtp_udp_host_bind_port %}
+			-p {{ matrix_jitsi_jvb_container_rtp_udp_host_bind_port }}:{{ matrix_jitsi_jvb_rtp_udp_port }}/udp \
+			{% endif %}
+			{% if matrix_jitsi_jvb_container_rtp_tcp_host_bind_port %}
+			-p {{ matrix_jitsi_jvb_container_rtp_tcp_host_bind_port }}:{{ matrix_jitsi_jvb_rtp_tcp_port }} \
+			{% endif %}
+			{% if matrix_jitsi_jvb_container_colibri_ws_host_bind_port %}
+			-p {{ matrix_jitsi_jvb_container_colibri_ws_host_bind_port }}:9090 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_jitsi_jvb_config_path }},dst=/config \
+			{% for arg in matrix_jitsi_jvb_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_jitsi_jvb_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-jvb 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-jvb 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-jitsi-jvb
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-jitsi/templates/prosody/env.j2 b/roles/matrix-jitsi/templates/prosody/env.j2
new file mode 100644
index 000000000..38b2456c6
--- /dev/null
+++ b/roles/matrix-jitsi/templates/prosody/env.j2
@@ -0,0 +1,49 @@
+AUTH_TYPE={{ matrix_jitsi_auth_type }}
+
+ENABLE_AUTH={{ 1 if matrix_jitsi_enable_auth else 0 }}
+ENABLE_GUESTS={{ 1 if matrix_jitsi_enable_guests else 0 }}
+
+PUBLIC_URL={{ matrix_jitsi_web_public_url }}
+
+LDAP_URL={{ matrix_jitsi_ldap_url }}
+LDAP_BASE={{ matrix_jitsi_ldap_base }}
+LDAP_BINDDN={{ matrix_jitsi_ldap_binddn }}
+LDAP_BINDPW={{ matrix_jitsi_ldap_bindpw }}
+LDAP_FILTER={{ matrix_jitsi_ldap_filter }}
+LDAP_AUTH_METHOD={{ matrix_jitsi_ldap_auth_method }}
+LDAP_VERSION={{ matrix_jitsi_ldap_version }}
+LDAP_USE_TLS={{ 1 if matrix_jitsi_ldap_use_tls else 0 }}
+LDAP_TLS_CIPHERS={{ matrix_jitsi_ldap_tls_ciphers }}
+LDAP_TLS_CHECK_PEER={{ 1 if matrix_jitsi_ldap_tls_check_peer else 0 }}
+LDAP_TLS_CACERT_FILE={{ matrix_jitsi_ldap_tls_cacert_file }}
+LDAP_TLS_CACERT_DIR={{ matrix_jitsi_ldap_tls_cacert_dir }}
+LDAP_START_TLS={{ 1 if matrix_jitsi_ldap_start_tls else 0 }}
+
+XMPP_DOMAIN={{ matrix_jitsi_xmpp_domain }}
+XMPP_AUTH_DOMAIN={{ matrix_jitsi_xmpp_auth_domain }}
+XMPP_GUEST_DOMAIN={{ matrix_jitsi_xmpp_guest_domain }}
+XMPP_MUC_DOMAIN={{ matrix_jitsi_xmpp_muc_domain }}
+XMPP_INTERNAL_MUC_DOMAIN={{ matrix_jitsi_xmpp_internal_muc_domain }}
+
+XMPP_MODULES={{ matrix_jitsi_xmpp_modules }}
+XMPP_MUC_MODULES=
+XMPP_INTERNAL_MUC_MODULES=
+
+XMPP_RECORDER_DOMAIN={{ matrix_jitsi_recorder_domain }}
+
+JICOFO_COMPONENT_SECRET={{ matrix_jitsi_jicofo_component_secret }}
+JICOFO_AUTH_USER={{ matrix_jitsi_jicofo_auth_user }}
+JICOFO_AUTH_PASSWORD={{ matrix_jitsi_jicofo_auth_password }}
+
+JVB_AUTH_USER={{ matrix_jitsi_jvb_auth_user }}
+JVB_AUTH_PASSWORD={{ matrix_jitsi_jvb_auth_password }}
+
+JIBRI_XMPP_USER={{ matrix_jitsi_jibri_xmpp_user }}
+JIBRI_XMPP_PASSWORD={{ matrix_jitsi_jibri_xmpp_password }}
+
+JIBRI_RECORDER_USER={{ matrix_jitsi_jibri_recorder_user }}
+JIBRI_RECORDER_PASSWORD={{ matrix_jitsi_jibri_recorder_password }}
+
+ENABLE_LOBBY={{ 1 if matrix_jitsi_enable_lobby else 0 }}
+
+TZ={{ matrix_jitsi_timezone }}
diff --git a/roles/matrix-jitsi/templates/prosody/matrix-jitsi-prosody.service.j2 b/roles/matrix-jitsi/templates/prosody/matrix-jitsi-prosody.service.j2
new file mode 100644
index 000000000..4445e52bd
--- /dev/null
+++ b/roles/matrix-jitsi/templates/prosody/matrix-jitsi-prosody.service.j2
@@ -0,0 +1,37 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix jitsi-prosody server
+{% for service in matrix_jitsi_prosody_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-prosody 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-prosody 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-jitsi-prosody \
+			--log-driver=none \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_jitsi_prosody_container_http_host_bind_port %}
+			-p {{ matrix_jitsi_prosody_container_http_host_bind_port }}:5280 \
+			{% endif %}
+			--env-file={{ matrix_jitsi_prosody_base_path }}/env \
+			--mount type=bind,src={{ matrix_jitsi_prosody_config_path }},dst=/config \
+			--mount type=bind,src={{ matrix_jitsi_prosody_plugins_path }},dst=/prosody-plugins-custom \
+			{% for arg in matrix_jitsi_prosody_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_jitsi_prosody_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-prosody 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-prosody 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-jitsi-prosody
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-jitsi/templates/web/custom-config.js.j2 b/roles/matrix-jitsi/templates/web/custom-config.js.j2
new file mode 100644
index 000000000..bbe85798a
--- /dev/null
+++ b/roles/matrix-jitsi/templates/web/custom-config.js.j2
@@ -0,0 +1,18 @@
+config.defaultLanguage = {{ matrix_jitsi_web_config_defaultLanguage|to_json }};
+
+
+if (!config.hasOwnProperty('p2p')) config.p2p = {% raw %}{}{% endraw %};
+
+{% if matrix_jitsi_web_stun_servers|length > 0 %}
+config.p2p.stunServers = [
+	{% for url in matrix_jitsi_web_stun_servers %}
+		{ urls: {{ url|to_json }} }{% if not loop.last %},{% endif %}
+	{% endfor %}
+];
+{% endif %}
+
+{% if matrix_jitsi_etherpad_enabled %}
+config.etherpad_base = {{ (matrix_jitsi_etherpad_base + '/p/') |to_json }}
+{% endif %}
+
+{{ matrix_jitsi_web_custom_config_extension }}
diff --git a/roles/matrix-jitsi/templates/web/env.j2 b/roles/matrix-jitsi/templates/web/env.j2
new file mode 100644
index 000000000..7b763a3ca
--- /dev/null
+++ b/roles/matrix-jitsi/templates/web/env.j2
@@ -0,0 +1,42 @@
+ENABLE_AUTH={{ 1 if matrix_jitsi_enable_auth else 0 }}
+ENABLE_GUESTS={{ 1 if matrix_jitsi_enable_guests else 0 }}
+
+ENABLE_TRANSCRIPTIONS={{ 1 if matrix_jitsi_enable_transcriptions else 0 }}
+
+ENABLE_P2P={{ 1 if matrix_jitsi_enable_p2p else 0 }}
+
+DISABLE_HTTPS=1
+
+JICOFO_AUTH_USER={{ matrix_jitsi_jicofo_auth_user }}
+
+PUBLIC_URL={{ matrix_jitsi_web_public_url }}
+
+XMPP_DOMAIN={{ matrix_jitsi_xmpp_domain }}
+XMPP_AUTH_DOMAIN={{ matrix_jitsi_xmpp_auth_domain }}
+XMPP_BOSH_URL_BASE={{ matrix_jitsi_xmpp_bosh_url_base }}
+XMPP_GUEST_DOMAIN={{ matrix_jitsi_xmpp_guest_domain }}
+XMPP_MUC_DOMAIN={{ matrix_jitsi_xmpp_muc_domain }}
+XMPP_RECORDER_DOMAIN={{ matrix_jitsi_recorder_domain }}
+
+TZ={{ matrix_jitsi_timezone }}
+
+JIBRI_BREWERY_MUC={{ matrix_jitsi_jibri_brewery_muc }}
+JIBRI_PENDING_TIMEOUT={{ matrix_jitsi_jibri_pending_timeout }}
+JIBRI_XMPP_USER={{ matrix_jitsi_jibri_xmpp_user }}
+JIBRI_XMPP_PASSWORD={{ matrix_jitsi_jibri_xmpp_password }}
+JIBRI_RECORDER_USER={{ matrix_jitsi_jibri_recorder_user }}
+JIBRI_RECORDER_PASSWORD={{ matrix_jitsi_jibri_recorder_password }}
+
+ENABLE_RECORDING={{ 1 if matrix_jitsi_enable_recording else 0 }}
+
+RESOLUTION={{ matrix_jitsi_web_config_resolution_height_ideal_and_max }}
+RESOLUTION_MIN={{ matrix_jitsi_web_config_resolution_height_min }}
+RESOLUTION_WIDTH={{ matrix_jitsi_web_config_resolution_width_ideal_and_max }}
+RESOLUTION_WIDTH_MIN={{ matrix_jitsi_web_config_resolution_width_min }}
+
+START_AUDIO_MUTED={{ matrix_jitsi_web_config_start_audio_muted_after_nth_participant }}
+START_VIDEO_MUTED={{ matrix_jitsi_web_config_start_video_muted_after_nth_participant }}
+
+ETHERPAD_URL_BASE={{ (matrix_jitsi_etherpad_base + '/') if matrix_jitsi_etherpad_enabled else ''}}
+
+{{ matrix_jitsi_web_environment_variables_extension }}
diff --git a/roles/matrix-jitsi/templates/web/interface_config.js.j2 b/roles/matrix-jitsi/templates/web/interface_config.js.j2
new file mode 100644
index 000000000..08ac02fe8
--- /dev/null
+++ b/roles/matrix-jitsi/templates/web/interface_config.js.j2
@@ -0,0 +1,295 @@
+/* eslint-disable no-unused-vars, no-var, max-len */
+/* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */
+
+var interfaceConfig = {
+    APP_NAME: {{ matrix_jitsi_web_interface_config_app_name|to_json }},
+    AUDIO_LEVEL_PRIMARY_COLOR: 'rgba(255,255,255,0.4)',
+    AUDIO_LEVEL_SECONDARY_COLOR: 'rgba(255,255,255,0.2)',
+
+    /**
+     * A UX mode where the last screen share participant is automatically
+     * pinned. Valid values are the string "remote-only" so remote participants
+     * get pinned but not local, otherwise any truthy value for all participants,
+     * and any falsy value to disable the feature.
+     *
+     * Note: this mode is experimental and subject to breakage.
+     */
+    AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only',
+    BRAND_WATERMARK_LINK: {{ matrix_jitsi_web_interface_config_brand_watermark_link|to_json }},
+
+    CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it
+    /**
+     * Whether the connection indicator icon should hide itself based on
+     * connection strength. If true, the connection indicator will remain
+     * displayed while the participant has a weak connection and will hide
+     * itself after the CONNECTION_INDICATOR_HIDE_TIMEOUT when the connection is
+     * strong.
+     *
+     * @type {boolean}
+     */
+    CONNECTION_INDICATOR_AUTO_HIDE_ENABLED: true,
+
+    /**
+     * How long the connection indicator should remain displayed before hiding.
+     * Used in conjunction with CONNECTION_INDICATOR_AUTOHIDE_ENABLED.
+     *
+     * @type {number}
+     */
+    CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT: 5000,
+
+    /**
+     * If true, hides the connection indicators completely.
+     *
+     * @type {boolean}
+     */
+    CONNECTION_INDICATOR_DISABLED: false,
+
+    DEFAULT_BACKGROUND: '#474747',
+    DEFAULT_LOCAL_DISPLAY_NAME: 'me',
+    DEFAULT_LOGO_URL: 'images/watermark.svg',
+    DEFAULT_REMOTE_DISPLAY_NAME: 'Fellow Jitster',
+    DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg',
+
+    DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
+
+    DISABLE_FOCUS_INDICATOR: false,
+
+    /**
+     * If true, notifications regarding joining/leaving are no longer displayed.
+     */
+    DISABLE_JOIN_LEAVE_NOTIFICATIONS: false,
+
+    /**
+     * If true, presence status: busy, calling, connected etc. is not displayed.
+     */
+    DISABLE_PRESENCE_STATUS: false,
+
+    /**
+     * Whether the ringing sound in the call/ring overlay is disabled. If
+     * {@code undefined}, defaults to {@code false}.
+     *
+     * @type {boolean}
+     */
+    DISABLE_RINGING: false,
+
+    /**
+     * Whether the speech to text transcription subtitles panel is disabled.
+     * If {@code undefined}, defaults to {@code false}.
+     *
+     * @type {boolean}
+     */
+    DISABLE_TRANSCRIPTION_SUBTITLES: {{ matrix_jitsi_web_interface_config_disable_transcription_subtitles|to_json }},
+
+    /**
+     * Whether or not the blurred video background for large video should be
+     * displayed on browsers that can support it.
+     */
+    DISABLE_VIDEO_BACKGROUND: false,
+
+    DISPLAY_WELCOME_FOOTER: true,
+    DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD: false,
+    DISPLAY_WELCOME_PAGE_CONTENT: {{ matrix_jitsi_web_interface_config_display_welcome_page_content|to_json }},
+    DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false,
+
+    ENABLE_DIAL_OUT: true,
+
+    ENABLE_FEEDBACK_ANIMATION: false, // Enables feedback star animation.
+
+    FILM_STRIP_MAX_HEIGHT: 120,
+
+    GENERATE_ROOMNAMES_ON_WELCOME_PAGE: {{ matrix_jitsi_web_interface_config_generate_room_names_on_welcome_page|to_json }},
+
+    /**
+     * Hide the logo on the deep linking pages.
+     */
+    HIDE_DEEP_LINKING_LOGO: false,
+
+    /**
+     * Hide the invite prompt in the header when alone in the meeting.
+     */
+    HIDE_INVITE_MORE_HEADER: false,
+
+    INITIAL_TOOLBAR_TIMEOUT: 20000,
+    JITSI_WATERMARK_LINK: {{ matrix_jitsi_web_interface_config_jitsi_watermark_link|to_json }},
+
+    LANG_DETECTION: {{ matrix_jitsi_web_interface_config_lang_detection|to_json }}, // Allow i18n to detect the system language
+    LIVE_STREAMING_HELP_LINK: 'https://jitsi.org/live', // Documentation reference for the live streaming feature.
+    LOCAL_THUMBNAIL_RATIO: 16 / 9, // 16:9
+
+    /**
+     * Maximum coefficient of the ratio of the large video to the visible area
+     * after the large video is scaled to fit the window.
+     *
+     * @type {number}
+     */
+    MAXIMUM_ZOOMING_COEFFICIENT: 1.3,
+
+    /**
+     * Whether the mobile app Jitsi Meet is to be promoted to participants
+     * attempting to join a conference in a mobile Web browser. If
+     * {@code undefined}, defaults to {@code true}.
+     *
+     * @type {boolean}
+     */
+    MOBILE_APP_PROMO: true,
+
+    /**
+     * Specify custom URL for downloading android mobile app.
+     */
+    MOBILE_DOWNLOAD_LINK_ANDROID: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
+
+    /**
+     * Specify custom URL for downloading f droid app.
+     */
+    MOBILE_DOWNLOAD_LINK_F_DROID: 'https://f-droid.org/en/packages/org.jitsi.meet/',
+
+    /**
+     * Specify URL for downloading ios mobile app.
+     */
+    MOBILE_DOWNLOAD_LINK_IOS: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
+
+    NATIVE_APP_NAME: {{ matrix_jitsi_web_interface_config_native_app_name|to_json }},
+
+    // Names of browsers which should show a warning stating the current browser
+    // has a suboptimal experience. Browsers which are not listed as optimal or
+    // unsupported are considered suboptimal. Valid values are:
+    // chrome, chromium, edge, electron, firefox, nwjs, opera, safari
+    OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'nwjs', 'electron', 'safari' ],
+
+    POLICY_LOGO: null,
+    PROVIDER_NAME: {{ matrix_jitsi_web_interface_config_provider_name|to_json }},
+
+    /**
+     * If true, will display recent list
+     *
+     * @type {boolean}
+     */
+    RECENT_LIST_ENABLED: true,
+    REMOTE_THUMBNAIL_RATIO: 1, // 1:1
+
+    SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
+    SHOW_BRAND_WATERMARK: {{ matrix_jitsi_web_interface_config_show_brand_watermark|to_json }},
+
+    /**
+    * Decides whether the chrome extension banner should be rendered on the landing page and during the meeting.
+    * If this is set to false, the banner will not be rendered at all. If set to true, the check for extension(s)
+    * being already installed is done before rendering.
+    */
+    SHOW_CHROME_EXTENSION_BANNER: false,
+
+    SHOW_DEEP_LINKING_IMAGE: {{ matrix_jitsi_web_interface_config_show_deep_linking_image|to_json }},
+    SHOW_JITSI_WATERMARK: {{ matrix_jitsi_web_interface_config_show_jitsi_watermark|to_json }},
+    SHOW_POWERED_BY: {{ matrix_jitsi_web_interface_config_show_powered_by|to_json }},
+    SHOW_PROMOTIONAL_CLOSE_PAGE: false,
+
+    /*
+     * If indicated some of the error dialogs may point to the support URL for
+     * help.
+     */
+    SUPPORT_URL: 'https://community.jitsi.org/',
+
+    TOOLBAR_ALWAYS_VISIBLE: false,
+
+    /**
+     * The name of the toolbar buttons to display in the toolbar, including the
+     * "More actions" menu. If present, the button will display. Exceptions are
+     * "livestreaming" and "recording" which also require being a moderator and
+     * some values in config.js to be enabled. Also, the "profile" button will
+     * not display for users with a JWT.
+     * Notes:
+     * - it's impossible to choose which buttons go in the "More actions" menu
+     * - it's impossible to control the placement of buttons
+     * - 'desktop' controls the "Share your screen" button
+     */
+    TOOLBAR_BUTTONS: [
+		{% if matrix_jitsi_enable_transcriptions %}
+            'closedcaptions',
+		{% endif %}
+		{% if matrix_jitsi_enable_recording %}
+            'recording',
+		{% endif %}
+        'microphone', 'camera', 'desktop', 'embedmeeting', 'fullscreen',
+        'fodeviceselection', 'hangup', 'profile', 'chat',
+        'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
+        'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
+        'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security'
+    ],
+
+    TOOLBAR_TIMEOUT: 4000,
+
+    // Browsers, in addition to those which do not fully support WebRTC, that
+    // are not supported and should show the unsupported browser page.
+    UNSUPPORTED_BROWSERS: [],
+
+    /**
+     * Whether to show thumbnails in filmstrip as a column instead of as a row.
+     */
+    VERTICAL_FILMSTRIP: true,
+
+    // Determines how the video would fit the screen. 'both' would fit the whole
+    // screen, 'height' would fit the original video height to the height of the
+    // screen, 'width' would fit the original video width to the width of the
+    // screen respecting ratio.
+    VIDEO_LAYOUT_FIT: 'both',
+
+    /**
+     * If true, hides the video quality label indicating the resolution status
+     * of the current large video.
+     *
+     * @type {boolean}
+     */
+    VIDEO_QUALITY_LABEL_DISABLED: false,
+
+    /**
+     * How many columns the tile view can expand to. The respected range is
+     * between 1 and 5.
+     */
+    // TILE_VIEW_MAX_COLUMNS: 5,
+
+    /**
+     * Specify Firebase dynamic link properties for the mobile apps.
+     */
+    // MOBILE_DYNAMIC_LINK: {
+    //    APN: 'org.jitsi.meet',
+    //    APP_CODE: 'w2atb',
+    //    CUSTOM_DOMAIN: undefined,
+    //    IBI: 'com.atlassian.JitsiMeet.ios',
+    //    ISI: '1165103905'
+    // },
+
+    /**
+     * Specify mobile app scheme for opening the app from the mobile browser.
+     */
+    // APP_SCHEME: 'org.jitsi.meet',
+
+    /**
+     * Specify the Android app package name.
+     */
+    // ANDROID_APP_PACKAGE: 'org.jitsi.meet',
+
+    /**
+     * Override the behavior of some notifications to remain displayed until
+     * explicitly dismissed through a user action. The value is how long, in
+     * milliseconds, those notifications should remain displayed.
+     */
+    // ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT: 15000,
+
+    // List of undocumented settings
+    /**
+     INDICATOR_FONT_SIZES
+     PHONE_NUMBER_REGEX
+    */
+
+    // Allow all above example options to include a trailing comma and
+    // prevent fear when commenting out the last value.
+    // eslint-disable-next-line sort-keys
+    makeJsonParserHappy: 'even if last key had a trailing comma'
+
+    // No configuration value should follow this line.
+};
+
+
+{{ matrix_jitsi_web_custom_interface_config_extension }}
+
+
+/* eslint-enable no-unused-vars, no-var, max-len */
diff --git a/roles/matrix-jitsi/templates/web/matrix-jitsi-web.service.j2 b/roles/matrix-jitsi/templates/web/matrix-jitsi-web.service.j2
new file mode 100644
index 000000000..6ae2074d5
--- /dev/null
+++ b/roles/matrix-jitsi/templates/web/matrix-jitsi-web.service.j2
@@ -0,0 +1,37 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix jitsi-web server
+{% for service in matrix_jitsi_web_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-web 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-web 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-jitsi-web \
+			--log-driver=none \
+			--network={{ matrix_docker_network }} \
+			--env-file={{ matrix_jitsi_web_base_path }}/env \
+			{% if matrix_jitsi_web_container_http_host_bind_port %}
+			-p {{ matrix_jitsi_web_container_http_host_bind_port }}:80 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_jitsi_web_config_path }},dst=/config \
+			--mount type=bind,src={{ matrix_jitsi_web_transcripts_path }},dst=/usr/share/jitsi-meet/transcripts \
+			{% for arg in matrix_jitsi_web_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_jitsi_web_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-jitsi-web 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-jitsi-web 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-jitsi-web
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-ma1sd/defaults/main.yml b/roles/matrix-ma1sd/defaults/main.yml
new file mode 100644
index 000000000..7ab0d15e2
--- /dev/null
+++ b/roles/matrix-ma1sd/defaults/main.yml
@@ -0,0 +1,163 @@
+# ma1sd is a Federated Matrix Identity Server
+# See: https://github.com/ma1uta/ma1sd
+
+matrix_ma1sd_enabled: true
+
+matrix_ma1sd_container_image_self_build: false
+matrix_ma1sd_container_image_self_build_repo: "https://github.com/ma1uta/ma1sd.git"
+matrix_ma1sd_container_image_self_build_branch: "{{ matrix_ma1sd_version }}"
+
+matrix_ma1sd_architecture: "amd64"
+
+matrix_ma1sd_version: "2.4.0"
+
+matrix_ma1sd_docker_image: "{{ matrix_ma1sd_docker_image_name_prefix }}ma1uta/ma1sd:{{ matrix_ma1sd_version }}-{{ matrix_ma1sd_architecture }}"
+matrix_ma1sd_docker_image_name_prefix: "{{ 'localhost/' if matrix_ma1sd_container_image_self_build else matrix_container_global_registry_prefix }}"
+matrix_ma1sd_docker_image_force_pull: "{{ matrix_ma1sd_docker_image.endswith(':latest') }}"
+
+matrix_ma1sd_base_path: "{{ matrix_base_data_path }}/ma1sd"
+# We need the docker src directory to be named ma1sd. See: https://github.com/spantaleev/matrix-docker-ansible-deploy/pull/588
+matrix_ma1sd_docker_src_files_path: "{{ matrix_ma1sd_base_path }}/docker-src/ma1sd"
+matrix_ma1sd_config_path: "{{ matrix_ma1sd_base_path }}/config"
+matrix_ma1sd_data_path: "{{ matrix_ma1sd_base_path }}/data"
+
+# Controls whether the matrix-ma1sd container exposes its HTTP port (tcp/8090 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8090"), or empty string to not expose.
+matrix_ma1sd_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_ma1sd_container_extra_arguments: []
+
+# List of systemd services that matrix-ma1sd.service depends on
+matrix_ma1sd_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-ma1sd.service wants
+matrix_ma1sd_systemd_wanted_services_list: []
+
+# Your identity server is private by default.
+# To ensure maximum discovery, you can make your identity server
+# also forward lookups to the central matrix.org Identity server
+# (at the cost of potentially leaking all your contacts information).
+# Enabling this is discouraged. Learn more here: https://github.com/ma1uta/ma1sd/blob/master/docs/features/identity.md#lookups
+matrix_ma1sd_matrixorg_forwarding_enabled: false
+
+
+# Database-related configuration fields.
+#
+# To use SQLite, stick to these defaults.
+#
+# To use Postgres:
+# - change the engine (`matrix_ma1sd_database_engine: 'postgres'`)
+# - adjust your database credentials via the `matrix_ma1sd_postgres_*` variables
+matrix_ma1sd_database_engine: 'sqlite'
+
+matrix_ma1sd_sqlite_database_path_local: "{{ matrix_ma1sd_data_path }}/ma1sd.db"
+matrix_ma1sd_sqlite_database_path_in_container: "/var/ma1sd/ma1sd.db"
+
+matrix_ma1sd_database_username: 'matrix_ma1sd'
+matrix_ma1sd_database_password: 'some-password'
+matrix_ma1sd_database_hostname: 'matrix-postgres'
+matrix_ma1sd_database_port: 5432
+matrix_ma1sd_database_name: 'matrix_ma1sd'
+
+matrix_ma1sd_database_connection_string: 'postgresql://{{ matrix_ma1sd_database_username }}:{{ matrix_ma1sd_database_password }}@{{ matrix_ma1sd_database_hostname }}:{{ matrix_ma1sd_database_port }}/{{ matrix_ma1sd_database_name }}'
+
+
+# ma1sd has serveral supported identity stores.
+# One of them is storing identities directly in Synapse's database.
+# Learn more here: https://github.com/ma1uta/ma1sd/blob/master/docs/stores/synapse.md
+matrix_ma1sd_synapsesql_enabled: false
+matrix_ma1sd_synapsesql_type: ""
+matrix_ma1sd_synapsesql_connection: ""
+
+# Setting up email-sending settings is required for using ma1sd.
+matrix_ma1sd_threepid_medium_email_identity_from: "matrix@{{ matrix_domain }}"
+matrix_ma1sd_threepid_medium_email_connectors_smtp_host: ""
+matrix_ma1sd_threepid_medium_email_connectors_smtp_port: 587
+matrix_ma1sd_threepid_medium_email_connectors_smtp_tls: 1
+matrix_ma1sd_threepid_medium_email_connectors_smtp_login: ""
+matrix_ma1sd_threepid_medium_email_connectors_smtp_password: ""
+
+# DNS overwrites are useful for telling ma1sd how it can reach the homeserver directly.
+# Useful when reverse-proxying certain URLs (e.g. `/_matrix/client/r0/user_directory/search`) to ma1sd,
+# so that ma1sd can rewrite the original URL to one that would reach the homeserver.
+matrix_ma1sd_dns_overwrite_enabled: false
+matrix_ma1sd_dns_overwrite_homeserver_client_name: "{{ matrix_server_fqn_matrix }}"
+matrix_ma1sd_dns_overwrite_homeserver_client_value: "http://matrix-synapse:8008"
+
+# Override the default session templates
+# To use this, fill in the template variables with the full desired template as a multi-line YAML variable
+#
+# More info:
+# https://github.com/ma1uta/ma1sd/blob/master/docs/threepids/session/session-views.md
+matrix_ma1sd_view_session_custom_templates_enabled: false
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/templates/session/tokenSubmitSuccess.html
+matrix_ma1sd_view_session_custom_onTokenSubmit_success_template: ""
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/templates/session/tokenSubmitFailure.html
+matrix_ma1sd_view_session_custom_onTokenSubmit_failure_template: ""
+
+# Override the default email templates
+# To use this, fill in the template variables with the full desired template as a multi-line YAML variable
+#
+# More info:
+# https://github.com/ma1uta/ma1sd/blob/master/docs/threepids/notification/template-generator.md
+# https://github.com/ma1uta/ma1sd/tree/master/src/main/resources/threepids/email
+matrix_ma1sd_threepid_medium_email_custom_templates_enabled: false
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/threepids/email/invite-template.eml
+matrix_ma1sd_threepid_medium_email_custom_invite_template: ""
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/threepids/email/validate-template.eml
+matrix_ma1sd_threepid_medium_email_custom_session_validation_template: ""
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/threepids/email/unbind-notification.eml
+matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template: ""
+# Defaults to: https://github.com/ma1uta/ma1sd/blob/master/src/main/resources/threepids/email/mxid-template.eml
+matrix_ma1sd_threepid_medium_email_custom_matrixid_template: ""
+
+# Controls whether the self-check feature should validate SSL certificates.
+matrix_ma1sd_self_check_validate_certificates: true
+
+# Controls ma1sd logging verbosity for troubleshooting.
+#
+# According to: https://github.com/ma1uta/ma1sd/blob/master/docs/troubleshooting.md#increase-verbosity
+matrix_ma1sd_verbose_logging: false
+
+# Setting up support for API prefixes
+matrix_ma1sd_v1_enabled: true
+matrix_ma1sd_v2_enabled: true
+
+# Fix for missing 3PIDS bug
+matrix_ma1sd_hashing_enabled: true
+
+# Default ma1sd configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_ma1sd_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_ma1sd_configuration_yaml: "{{ lookup('template', 'templates/ma1sd.yaml.j2') }}"
+
+matrix_ma1sd_configuration_extension_yaml: |
+  # Your custom YAML configuration for ma1sd goes here.
+  # This configuration extends the default starting configuration (`matrix_ma1sd_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_ma1sd_configuration_yaml`.
+  #
+  # Example configuration extension follows:
+  #
+  # ldap:
+  #   enabled: true
+  #   connection:
+  #     host: ldapHostnameOrIp
+  #     tls: false
+  #     port: 389
+  #     baseDNs: ['OU=Users,DC=example,DC=org']
+  #     bindDn: CN=My Ma1sd User,OU=Users,DC=example,DC=org
+  #     bindPassword: TheUserPassword
+
+matrix_ma1sd_configuration_extension: "{{ matrix_ma1sd_configuration_extension_yaml|from_yaml if matrix_ma1sd_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final ma1sd configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_ma1sd_configuration_yaml`.
+matrix_ma1sd_configuration: "{{ matrix_ma1sd_configuration_yaml|from_yaml|combine(matrix_ma1sd_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-ma1sd/tasks/init.yml b/roles/matrix-ma1sd/tasks/init.yml
new file mode 100644
index 000000000..04cc3a213
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/init.yml
@@ -0,0 +1,10 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_ma1sd_container_image_self_build and matrix_ma1sd_enabled|bool"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-ma1sd.service'] }}"
+  when: matrix_ma1sd_enabled|bool
diff --git a/roles/matrix-ma1sd/tasks/main.yml b/roles/matrix-ma1sd/tasks/main.yml
new file mode 100644
index 000000000..0b8a114e1
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/main.yml
@@ -0,0 +1,28 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_ma1sd_enabled|bool"
+  tags:
+    - setup-all
+    - setup-ma1sd
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: "run_setup|bool and matrix_ma1sd_enabled|bool"
+  tags:
+    - setup-all
+    - setup-ma1sd
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: "run_setup|bool and not matrix_ma1sd_enabled|bool"
+  tags:
+    - setup-all
+    - setup-ma1sd
+
+- import_tasks: "{{ role_path }}/tasks/self_check_ma1sd.yml"
+  delegate_to: 127.0.0.1
+  become: false
+  when: "run_self_check|bool and matrix_ma1sd_enabled|bool"
+  tags:
+    - self-check
diff --git a/roles/matrix-ma1sd/tasks/migrate_mxisd.yml b/roles/matrix-ma1sd/tasks/migrate_mxisd.yml
new file mode 100644
index 000000000..1d9662042
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/migrate_mxisd.yml
@@ -0,0 +1,72 @@
+---
+
+# This task is for migrating existing mxisd data when transitioning to the ma1sd fork.
+
+- name: Check for existent mxisd data
+  stat:
+    path: "{{ matrix_base_data_path }}/mxisd/data"
+  register: ma1sd_migrate_mxisd_data_dir_stat
+
+- name: Warn if mxisd data detected
+  debug:
+    msg: >
+      You seem to have an existing mxisd folder in `{{ matrix_base_data_path }}/mxisd`.
+      We are going to migrate it to ma1sd and rename the folder to mxisd.migrated.
+  when: "ma1sd_migrate_mxisd_data_dir_stat.stat.exists"
+
+- name: Check existence of old matrix-mxisd service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-mxisd.service"
+  register: matrix_mxisd_service_stat
+
+- name: Ensure matrix-mxisd is stopped
+  service:
+    name: matrix-mxisd
+    state: stopped
+    daemon_reload: yes
+  when: "matrix_mxisd_service_stat.stat.exists"
+
+- name: Check existence of matrix-ma1sd service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-ma1sd.service"
+  register: matrix_ma1sd_service_stat
+  when: "ma1sd_migrate_mxisd_data_dir_stat.stat.exists"
+
+- name: Ensure matrix-ma1sd is stopped
+  service:
+    name: matrix-ma1sd
+    state: stopped
+    daemon_reload: yes
+  when: "ma1sd_migrate_mxisd_data_dir_stat.stat.exists and matrix_ma1sd_service_stat.stat.exists"
+
+# We use shell commands for the migration, because the Ansible copy module cannot
+# recursively copy remote directories (like `/matrix/mxisd/data/sign.key`) in older versions of Ansible.
+- block:
+  - name: Copy mxisd data files to ma1sd folder
+    command: "cp -ar {{ matrix_base_data_path }}/mxisd/data {{ matrix_ma1sd_base_path }}"
+
+  - name: Check existence of mxisd.db file
+    stat:
+      path: "{{ matrix_ma1sd_data_path }}/mxisd.db"
+    register: matrix_ma1sd_mxisd_db_stat
+
+  - name: Rename database (mxisd.db -> ma1sd.db)
+    command: "mv {{ matrix_ma1sd_data_path }}/mxisd.db {{ matrix_ma1sd_data_path }}/ma1sd.db"
+    when: "matrix_ma1sd_mxisd_db_stat.stat.exists"
+
+  - name: Rename mxisd folder
+    command: "mv {{ matrix_base_data_path }}/mxisd {{ matrix_base_data_path }}/mxisd.migrated"
+  when: "ma1sd_migrate_mxisd_data_dir_stat.stat.exists"
+
+- name: Ensure outdated matrix-mxisd.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-mxisd.service"
+    state: absent
+  when: "matrix_mxisd_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after removing outdated matrix-mxisd.service
+  service:
+    daemon_reload: yes
+  when: "matrix_mxisd_service_stat.stat.exists"
+
+
diff --git a/roles/matrix-ma1sd/tasks/self_check_ma1sd.yml b/roles/matrix-ma1sd/tasks/self_check_ma1sd.yml
new file mode 100644
index 000000000..b8a7faaa3
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/self_check_ma1sd.yml
@@ -0,0 +1,22 @@
+---
+
+- set_fact:
+    ma1sd_url_endpoint_public: "https://{{ matrix_server_fqn_matrix }}/_matrix/identity/api/v1"
+
+- name: Check ma1sd Identity Service
+  uri:
+    url: "{{ ma1sd_url_endpoint_public }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_ma1sd_self_check_validate_certificates }}"
+  check_mode: no
+  register: result_ma1sd
+  ignore_errors: true
+
+- name: Fail if ma1sd Identity Service not working
+  fail:
+    msg: "Failed checking ma1sd is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ ma1sd_url_endpoint_public }}`). Is ma1sd running? Is port 443 open in your firewall? Full error: {{ result_ma1sd }}"
+  when: "result_ma1sd.failed or 'json' not in result_ma1sd"
+
+- name: Report working ma1sd Identity Service
+  debug:
+    msg: "ma1sd at `{{ matrix_server_fqn_matrix }}` is working (checked endpoint: `{{ ma1sd_url_endpoint_public }}`)"
diff --git a/roles/matrix-ma1sd/tasks/setup_install.yml b/roles/matrix-ma1sd/tasks/setup_install.yml
new file mode 100644
index 000000000..3f319eeff
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/setup_install.yml
@@ -0,0 +1,167 @@
+---
+
+- name: Ensure ma1sd paths exist
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_ma1sd_config_path }}", when: true }
+    - { path: "{{ matrix_ma1sd_data_path }}", when: true }
+    - { path: "{{ matrix_ma1sd_docker_src_files_path }}", when: "{{ matrix_ma1sd_container_image_self_build }}"}
+  when: "item.when|bool"
+
+- import_tasks: "{{ role_path }}/tasks/migrate_mxisd.yml"
+
+
+# These (SQLite -> Postgres) migration tasks are usually at the top,
+# but we'd like to run them after `migrate_mxisd.yml`, which requires the ma1sd paths to exist.
+- set_fact:
+    matrix_ma1sd_requires_restart: false
+
+- block:
+    - name: Check if an SQLite database already exists
+      stat:
+        path: "{{ matrix_ma1sd_sqlite_database_path_local }}"
+      register: matrix_ma1sd_sqlite_database_path_local_stat_result
+
+    - block:
+        - set_fact:
+            matrix_postgres_db_migration_request:
+              src: "{{ matrix_ma1sd_sqlite_database_path_local }}"
+              dst: "{{ matrix_ma1sd_database_connection_string }}"
+              caller: "{{ role_path|basename }}"
+              engine_variable_name: 'matrix_ma1sd_database_engine'
+              engine_old: 'sqlite'
+              systemd_services_to_stop: ['matrix-ma1sd.service']
+              pgloader_options: ['--with "quote identifiers"']
+
+        - import_tasks: "{{ role_path }}/../matrix-postgres/tasks/util/migrate_db_to_postgres.yml"
+
+        - set_fact:
+            matrix_ma1sd_requires_restart: true
+      when: "matrix_ma1sd_sqlite_database_path_local_stat_result.stat.exists|bool"
+  when: "matrix_ma1sd_database_engine == 'postgres'"
+
+- name: Ensure ma1sd image is pulled
+  docker_image:
+    name: "{{ matrix_ma1sd_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_ma1sd_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_ma1sd_docker_image_force_pull }}"
+  when: "not matrix_ma1sd_container_image_self_build|bool"
+
+- block:
+  - name: Ensure gradle is installed for self-building (Debian)
+    apt:
+      name:
+        - gradle
+      state: present
+      update_cache: yes
+    when: (ansible_os_family == 'Debian')
+
+  - name: Ensure gradle is installed for self-building (CentOS)
+    fail:
+      msg: "Installing gradle on CentOS is currently not supported, so self-building ma1sd cannot happen at this time"
+    when: ansible_distribution == 'CentOS'
+
+  - name: Ensure gradle is installed for self-building (Archlinux)
+    pacman:
+      name:
+        - gradle
+      state: latest
+      update_cache: yes
+    when: ansible_distribution == 'Archlinux'
+
+  - name: Ensure ma1sd repository is present on self-build
+    git:
+      repo: "{{ matrix_ma1sd_container_image_self_build_repo }}"
+      dest: "{{ matrix_ma1sd_docker_src_files_path }}"
+      version: "{{ matrix_ma1sd_container_image_self_build_branch }}"
+      force: "yes"
+    register: matrix_ma1sd_git_pull_results
+
+  - name: Ensure ma1sd Docker image is built
+    shell: "DOCKER_BUILDKIT=1 ./gradlew dockerBuild"
+    args:
+      chdir: "{{ matrix_ma1sd_docker_src_files_path }}"
+
+  - name: Ensure ma1sd Docker image is tagged correctly
+    docker_image:
+      # The build script always tags the image with 2 tags:
+      # - based on the branch/version: e.g. `ma1uta/ma1sd:2.4.0` (when on `2.4.0`)
+      #   or `ma1uta/ma1sd:2.4.0-19-ga71d32b` (when on a given commit for a pre-release)
+      # - generic one: `ma1uta/ma1sd:latest-dev`
+      #
+      # It's hard to predict the first one, so we'll use the latter.
+      name: "ma1uta/ma1sd:latest-dev"
+      repository: "{{ matrix_ma1sd_docker_image }}"
+      force_tag: yes
+      source: local
+  when: "matrix_ma1sd_container_image_self_build|bool"
+
+- name: Ensure ma1sd config installed
+  copy:
+    content: "{{ matrix_ma1sd_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_ma1sd_config_path }}/ma1sd.yaml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure custom view templates are installed, if any
+  copy:
+    content: "{{ item.value }}"
+    dest: "{{ matrix_ma1sd_config_path }}/{{ item.location }}"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - {value: "{{ matrix_ma1sd_view_session_custom_onTokenSubmit_success_template }}", location: 'tokenSubmitSuccess.html'}
+    - {value: "{{ matrix_ma1sd_view_session_custom_onTokenSubmit_failure_template }}", location: 'tokenSubmitFailure.html'}
+  when: "matrix_ma1sd_view_session_custom_templates_enabled|bool and item.value"
+
+- name: Ensure custom email templates are installed, if any
+  copy:
+    content: "{{ item.value }}"
+    dest: "{{ matrix_ma1sd_config_path }}/{{ item.location }}"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_invite_template }}", location: 'invite-template.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_session_validation_template }}", location: 'validate-template.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template }}", location: 'unbind-notification.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_matrixid_template }}", location: 'mxid-template.eml'}
+  when: "matrix_ma1sd_threepid_medium_email_custom_templates_enabled|bool and item.value"
+
+# Only cleaning up for people who define the respective templates
+- name: (Cleanup) Ensure custom email templates are not in data/ anymore (we've put them in config/)
+  file:
+    path: "{{ matrix_ma1sd_data_path }}/{{ item.location }}"
+    state: absent
+  with_items:
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_invite_template }}", location: 'invite-template.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_session_validation_template }}", location: 'validate-template.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template }}", location: 'unbind-notification.eml'}
+    - {value: "{{ matrix_ma1sd_threepid_medium_email_custom_matrixid_template }}", location: 'mxid-template.eml'}
+  when: "matrix_ma1sd_threepid_medium_email_custom_templates_enabled|bool and item.value"
+
+- name: Ensure matrix-ma1sd.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-ma1sd.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-ma1sd.service"
+    mode: 0644
+  register: matrix_ma1sd_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-ma1sd.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_ma1sd_systemd_service_result.changed|bool"
+
+- name: Ensure matrix-ma1sd.service restarted, if necessary
+  service:
+    name: "matrix-ma1sd.service"
+    state: restarted
+  when: "matrix_ma1sd_requires_restart|bool"
diff --git a/roles/matrix-ma1sd/tasks/setup_uninstall.yml b/roles/matrix-ma1sd/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..b36ab508f
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/setup_uninstall.yml
@@ -0,0 +1,35 @@
+---
+
+- name: Check existence of matrix-ma1sd service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-ma1sd.service"
+  register: matrix_ma1sd_service_stat
+
+- name: Ensure matrix-ma1sd is stopped
+  service:
+    name: matrix-ma1sd
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_ma1sd_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-ma1sd.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-ma1sd.service"
+    state: absent
+  when: "matrix_ma1sd_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-ma1sd.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_ma1sd_service_stat.stat.exists|bool"
+
+- name: Ensure Matrix ma1sd paths don't exist
+  file:
+    path: "{{ matrix_ma1sd_base_path }}"
+    state: absent
+
+- name: Ensure ma1sd Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_ma1sd_docker_image }}"
+    state: absent
diff --git a/roles/matrix-ma1sd/tasks/validate_config.yml b/roles/matrix-ma1sd/tasks/validate_config.yml
new file mode 100644
index 000000000..4ca25e7ec
--- /dev/null
+++ b/roles/matrix-ma1sd/tasks/validate_config.yml
@@ -0,0 +1,67 @@
+---
+
+- name: (Deprecation) Warn about ma1sd variables that are not used anymore
+  fail:
+    msg: >
+      The `{{ item }}` variable defined in your configuration is not used by this playbook anymore!
+      You'll need to adapt to the new way of extending ma1sd configuration.
+      See the CHANGELOG and the `matrix_ma1sd_configuration_extension_yaml` variable for more information and examples.
+  when: "item in vars"
+  with_items:
+    - 'matrix_ma1sd_ldap_enabled'
+    - 'matrix_ma1sd_ldap_connection_host'
+    - 'matrix_ma1sd_ldap_connection_tls'
+    - 'matrix_ma1sd_ldap_connection_port'
+    - 'matrix_ma1sd_ldap_connection_baseDn'
+    - 'matrix_ma1sd_ldap_connection_baseDns'
+    - 'matrix_ma1sd_ldap_connection_bindDn'
+    - 'matrix_ma1sd_ldap_connection_bindPassword'
+    - 'matrix_ma1sd_ldap_filter'
+    - 'matrix_ma1sd_ldap_attribute_uid_type'
+    - 'matrix_ma1sd_ldap_attribute_uid_value'
+    - 'matrix_ma1sd_ldap_connection_bindPassword'
+    - 'matrix_ma1sd_ldap_attribute_name'
+    - 'matrix_ma1sd_ldap_attribute_threepid_email'
+    - 'matrix_ma1sd_ldap_attribute_threepid_msisdn'
+    - 'matrix_ma1sd_ldap_identity_filter'
+    - 'matrix_ma1sd_ldap_identity_medium'
+    - 'matrix_ma1sd_ldap_auth_filter'
+    - 'matrix_ma1sd_ldap_directory_filter'
+    - 'matrix_ma1sd_template_config'
+
+- name: Ensure ma1sd configuration does not contain any dot-notation keys
+  fail:
+    msg: >
+      Since version 1.3.0, ma1sd will not accept property-style configuration keys.
+      You have defined a key (`{{ item.key }}`) which contains a dot.
+      Instead, use nesting. See: https://github.com/ma1uta/ma1sd/wiki/Upgrade-Notes#v130
+  when: "'.' in item.key"
+  with_dict: "{{ matrix_ma1sd_configuration }}"
+
+- name: Fail if required ma1sd settings not defined
+  fail:
+    msg: >
+      You need to define a required configuration setting (`{{ item }}`) for using ma1sd.
+  when: "vars[item] == ''"
+  with_items:
+    - "matrix_ma1sd_threepid_medium_email_connectors_smtp_host"
+
+- name: (Deprecation) Catch and report renamed ma1sd variables
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "vars | dict2items | selectattr('key', 'match', item.old) | list | items2dict"
+  with_items:
+    - {'old': 'matrix_ma1sd_container_expose_port', 'new': '<superseded by matrix_ma1sd_container_http_host_bind_port>'}
+    - {'old': 'matrix_ma1sd_threepid_medium_email_custom_unbind_fraudulent_template', 'new': 'matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template'}
+
+- name: (Deprecation) Catch and report mxisd variables
+  fail:
+    msg: >-
+      mxisd is deprecated and has been replaced with ma1sd (https://github.com/ma1uta/ma1sd), a compatible fork.
+      The playbook will migrate your existing mxisd configuration and data automatically, but you need to adjust variable names.
+      Please change your configuration (vars.yml) to rename all mxisd variables (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "vars | dict2items | selectattr('key', 'match', item.old) | list | items2dict"
+  with_items:
+    - {'old': 'matrix_mxisd_.*', 'new': 'matrix_ma1sd_.*'}
diff --git a/roles/matrix-ma1sd/templates/ma1sd.yaml.j2 b/roles/matrix-ma1sd/templates/ma1sd.yaml.j2
new file mode 100644
index 000000000..a4100adcb
--- /dev/null
+++ b/roles/matrix-ma1sd/templates/ma1sd.yaml.j2
@@ -0,0 +1,104 @@
+#jinja2: lstrip_blocks: True
+matrix:
+  domain: {{ matrix_domain }}
+  v1: {{ matrix_ma1sd_v1_enabled|to_json }}
+  v2: {{ matrix_ma1sd_v2_enabled|to_json }}
+
+server:
+  name: {{ matrix_server_fqn_matrix }}
+
+key:
+  path: /var/ma1sd/sign.key
+
+storage:
+  {% if matrix_ma1sd_database_engine == 'sqlite' %}
+    backend: sqlite
+    provider:
+      sqlite:
+        database: {{ matrix_ma1sd_sqlite_database_path_in_container|to_json }}
+  {% elif matrix_ma1sd_database_engine == 'postgres' %}
+    backend: postgresql
+    provider:
+      postgresql:
+        database: //{{ matrix_ma1sd_database_hostname }}:{{ matrix_ma1sd_database_port }}/{{ matrix_ma1sd_database_name }}
+        username: {{ matrix_ma1sd_database_username|to_json }}
+        password: {{ matrix_ma1sd_database_password|to_json }}
+  {% endif %}
+
+{% if matrix_ma1sd_dns_overwrite_enabled %}
+dns:
+  overwrite:
+    homeserver:
+      client:
+        - name: {{ matrix_ma1sd_dns_overwrite_homeserver_client_name }}
+          value: {{ matrix_ma1sd_dns_overwrite_homeserver_client_value }}
+{% endif %}
+
+{% if matrix_ma1sd_matrixorg_forwarding_enabled %}
+forward:
+  servers: ['matrix-org']
+{% endif %}
+
+threepid:
+  medium:
+    email:
+      identity:
+        from: {{ matrix_ma1sd_threepid_medium_email_identity_from }}
+      connectors:
+        smtp:
+          host: {{ matrix_ma1sd_threepid_medium_email_connectors_smtp_host }}
+          port: {{ matrix_ma1sd_threepid_medium_email_connectors_smtp_port }}
+          tls: {{ matrix_ma1sd_threepid_medium_email_connectors_smtp_tls }}
+          login: {{ matrix_ma1sd_threepid_medium_email_connectors_smtp_login }}
+          password: {{ matrix_ma1sd_threepid_medium_email_connectors_smtp_password }}
+{% if matrix_ma1sd_threepid_medium_email_custom_templates_enabled %}
+      generators:
+        template:
+          {% if matrix_ma1sd_threepid_medium_email_custom_invite_template %}
+          invite: '/etc/ma1sd/invite-template.eml'
+          {% endif %}
+          {% if matrix_ma1sd_threepid_medium_email_custom_session_validation_template or matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template %}
+          session:
+            {% if matrix_ma1sd_threepid_medium_email_custom_session_validation_template %}
+            validation: '/etc/ma1sd/validate-template.eml'
+            {% endif %}
+            {% if matrix_ma1sd_threepid_medium_email_custom_session_unbind_notification_template %}
+            unbind:
+              notification: '/etc/ma1sd/unbind-notification.eml'
+            {% endif %}
+          {% endif %}
+          {% if matrix_ma1sd_threepid_medium_email_custom_matrixid_template %}
+          generic:
+            matrixId: '/etc/ma1sd/mxid-template.eml'
+          {% endif %}
+{% endif %}
+
+{% if matrix_ma1sd_view_session_custom_templates_enabled %}
+view:
+  session:
+    onTokenSubmit:
+      {% if matrix_ma1sd_view_session_custom_onTokenSubmit_success_template %}
+      success: '/etc/ma1sd/tokenSubmitSuccess.html'
+      {% endif %}
+      {% if matrix_ma1sd_view_session_custom_onTokenSubmit_failure_template %}
+      failure: '/etc/ma1sd/tokenSubmitFailure.html'
+      {% endif %}
+{% endif %}
+
+{% if matrix_ma1sd_hashing_enabled %}
+hashing:
+  enabled: true # enable or disable the hash lookup MSC2140 (default is false)
+  pepperLength: 20 # length of the pepper value (default is 20)
+  rotationPolicy: per_requests # or `per_seconds` how often the hashes will be updating
+  hashStorageType: sql # or `in_memory` where the hashes will be stored
+  algorithms:
+    - none   # the same as v1 bulk lookup
+    - sha256 # hash the 3PID and pepper.
+  delay: 2m # how often hashes will be updated if rotation policy = per_seconds (default is 10s)
+  requests: 10
+{% endif %}
+
+synapseSql:
+  enabled: {{ matrix_ma1sd_synapsesql_enabled|to_json }}
+  type: {{ matrix_ma1sd_synapsesql_type|to_json }}
+  connection: {{ matrix_ma1sd_synapsesql_connection|to_json }}
diff --git a/roles/matrix-ma1sd/templates/systemd/matrix-ma1sd.service.j2 b/roles/matrix-ma1sd/templates/systemd/matrix-ma1sd.service.j2
new file mode 100644
index 000000000..c2adffc08
--- /dev/null
+++ b/roles/matrix-ma1sd/templates/systemd/matrix-ma1sd.service.j2
@@ -0,0 +1,48 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix ma1sd Identity server
+{% for service in matrix_ma1sd_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_ma1sd_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-ma1sd 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-ma1sd 2>/dev/null'
+
+# ma1sd writes an SQLite shared library (libsqlitejdbc.so) to /tmp and executes it from there,
+# so /tmp needs to be mounted with an exec option.
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-ma1sd \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,exec,nosuid,size=10m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_ma1sd_container_http_host_bind_port %}
+			-p {{ matrix_ma1sd_container_http_host_bind_port }}:8090 \
+			{% endif %}
+			{% if matrix_ma1sd_verbose_logging %}
+			-e MA1SD_LOG_LEVEL=debug \
+			{% endif %}
+			--mount type=bind,src={{ matrix_ma1sd_config_path }},dst=/etc/ma1sd,ro \
+			--mount type=bind,src={{ matrix_ma1sd_data_path }},dst=/var/ma1sd \
+			{% for arg in matrix_ma1sd_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_ma1sd_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-ma1sd 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-ma1sd 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-ma1sd
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-ma1sd/vars/main.yml b/roles/matrix-ma1sd/vars/main.yml
new file mode 100644
index 000000000..b6c97a593
--- /dev/null
+++ b/roles/matrix-ma1sd/vars/main.yml
@@ -0,0 +1,5 @@
+---
+
+# Doing `|from_yaml` when the extension contains nothing yields an empty string ("").
+# We need to ensure it's a dictionary or `|combine` (when building `matrix_ma1sd_configuration`) will fail later.
+matrix_ma1sd_configuration_extension: "{{ matrix_ma1sd_configuration_extension_yaml|from_yaml if matrix_ma1sd_configuration_extension_yaml|from_yaml else {} }}"
diff --git a/roles/matrix-mailer/defaults/main.yml b/roles/matrix-mailer/defaults/main.yml
new file mode 100644
index 000000000..8ca1a8a39
--- /dev/null
+++ b/roles/matrix-mailer/defaults/main.yml
@@ -0,0 +1,31 @@
+matrix_mailer_enabled: true
+
+matrix_mailer_base_path: "{{ matrix_base_data_path }}/mailer"
+
+matrix_mailer_container_image_self_build: false
+matrix_mailer_container_image_self_build_repository_url: "https://github.com/devture/exim-relay"
+matrix_mailer_container_image_self_build_src_files_path: "{{ matrix_mailer_base_path }}/docker-src"
+matrix_mailer_container_image_self_build_version: "{{ matrix_mailer_docker_image.split(':')[1] }}"
+
+matrix_mailer_version: 4.94.2-r0-2
+matrix_mailer_docker_image: "{{ matrix_mailer_docker_image_name_prefix }}devture/exim-relay:{{ matrix_mailer_version }}"
+matrix_mailer_docker_image_name_prefix: "{{ 'localhost/' if matrix_mailer_container_image_self_build else matrix_container_global_registry_prefix }}"
+matrix_mailer_docker_image_force_pull: "{{ matrix_mailer_docker_image.endswith(':latest') }}"
+
+# The user/group that the container runs with.
+# These match the `exim` user/group within the container image.
+matrix_mailer_container_user_uid: 100
+matrix_mailer_container_user_gid: 101
+
+# A list of extra arguments to pass to the container
+matrix_mailer_container_extra_arguments: []
+
+matrix_mailer_hostname: "{{ matrix_server_fqn_matrix }}"
+
+matrix_mailer_sender_address: "matrix@{{ matrix_domain }}"
+matrix_mailer_relay_use: false
+matrix_mailer_relay_host_name: "mail.example.com"
+matrix_mailer_relay_host_port: 587
+matrix_mailer_relay_auth: false
+matrix_mailer_relay_auth_username: ""
+matrix_mailer_relay_auth_password: ""
diff --git a/roles/matrix-mailer/tasks/init.yml b/roles/matrix-mailer/tasks/init.yml
new file mode 100644
index 000000000..d07380f0e
--- /dev/null
+++ b/roles/matrix-mailer/tasks/init.yml
@@ -0,0 +1,10 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_mailer_container_image_self_build and matrix_mailer_enabled"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-mailer.service'] }}"
+  when: matrix_mailer_enabled|bool
diff --git a/roles/matrix-mailer/tasks/main.yml b/roles/matrix-mailer/tasks/main.yml
new file mode 100644
index 000000000..f636614e0
--- /dev/null
+++ b/roles/matrix-mailer/tasks/main.yml
@@ -0,0 +1,9 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup_mailer.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-mailer
diff --git a/roles/matrix-mailer/tasks/setup_mailer.yml b/roles/matrix-mailer/tasks/setup_mailer.yml
new file mode 100644
index 000000000..251a52da5
--- /dev/null
+++ b/roles/matrix-mailer/tasks/setup_mailer.yml
@@ -0,0 +1,107 @@
+---
+
+#
+# Tasks related to setting up the mailer
+#
+
+- name: Ensure mailer base path exists
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_mailer_base_path }}", when: true }
+    - { path: "{{ matrix_mailer_container_image_self_build_src_files_path }}", when: "{{ matrix_mailer_container_image_self_build }}" }
+  when: "matrix_mailer_enabled|bool and item.when"
+
+- name: Ensure mailer environment variables file created
+  template:
+    src: "{{ role_path }}/templates/env-mailer.j2"
+    dest: "{{ matrix_mailer_base_path }}/env-mailer"
+    mode: 0640
+  when: matrix_mailer_enabled|bool
+
+- name: Ensure exim-relay repository is present on self-build
+  git:
+    repo: "{{ matrix_mailer_container_image_self_build_repository_url }}"
+    dest: "{{ matrix_mailer_container_image_self_build_src_files_path }}"
+    version: "{{ matrix_mailer_container_image_self_build_version }}"
+    force: "yes"
+  register: matrix_mailer_git_pull_results
+  when: "matrix_mailer_enabled|bool and matrix_mailer_container_image_self_build|bool"
+
+- name: Ensure exim-relay Docker image is built
+  docker_image:
+    name: "{{ matrix_mailer_docker_image }}"
+    source: build
+    force_source: "{{ matrix_mailer_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_mailer_git_pull_results.changed }}"
+    build:
+      dockerfile: Dockerfile
+      path: "{{ matrix_mailer_container_image_self_build_src_files_path }}"
+      pull: yes
+  when: "matrix_mailer_enabled|bool and matrix_mailer_container_image_self_build|bool"
+
+- name: Ensure exim-relay image is pulled
+  docker_image:
+    name: "{{ matrix_mailer_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_mailer_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_mailer_docker_image_force_pull }}"
+  when: "matrix_mailer_enabled|bool and not matrix_mailer_container_image_self_build|bool"
+
+- name: Ensure matrix-mailer.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-mailer.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-mailer.service"
+    mode: 0644
+  register: matrix_mailer_systemd_service_result
+  when: matrix_mailer_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-mailer.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_mailer_enabled|bool and matrix_mailer_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of the mailer (if it was previously enabled)
+#
+
+- name: Check existence of matrix-mailer service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-mailer.service"
+  register: matrix_mailer_service_stat
+  when: "not matrix_mailer_enabled|bool"
+
+- name: Ensure matrix-mailer is stopped
+  service:
+    name: matrix-mailer
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_mailer_enabled|bool and matrix_mailer_service_stat.stat.exists"
+
+- name: Ensure matrix-mailer.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-mailer.service"
+    state: absent
+  when: "not matrix_mailer_enabled|bool and matrix_mailer_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-mailer.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_mailer_enabled|bool and matrix_mailer_service_stat.stat.exists"
+
+- name: Ensure Matrix mailer environment variables path doesn't exist
+  file:
+    path: "{{ matrix_mailer_base_path }}"
+    state: absent
+  when: "not matrix_mailer_enabled|bool"
+
+- name: Ensure mailer Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_mailer_docker_image }}"
+    state: absent
+  when: "not matrix_mailer_enabled|bool"
diff --git a/roles/matrix-mailer/templates/env-mailer.j2 b/roles/matrix-mailer/templates/env-mailer.j2
new file mode 100644
index 000000000..eb3f86999
--- /dev/null
+++ b/roles/matrix-mailer/templates/env-mailer.j2
@@ -0,0 +1,9 @@
+#jinja2: lstrip_blocks: "True"
+{% if matrix_mailer_relay_use %}
+SMARTHOST={{ matrix_mailer_relay_host_name }}::{{ matrix_mailer_relay_host_port }}
+{% endif %}
+{% if matrix_mailer_relay_auth %}
+SMTP_USERNAME={{ matrix_mailer_relay_auth_username }}
+SMTP_PASSWORD={{ matrix_mailer_relay_auth_password }}
+{% endif %}
+HOSTNAME={{ matrix_mailer_hostname }}
diff --git a/roles/matrix-mailer/templates/systemd/matrix-mailer.service.j2 b/roles/matrix-mailer/templates/systemd/matrix-mailer.service.j2
new file mode 100644
index 000000000..bf5a2e42a
--- /dev/null
+++ b/roles/matrix-mailer/templates/systemd/matrix-mailer.service.j2
@@ -0,0 +1,37 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix mailer
+After=docker.service
+Requires=docker.service
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-mailer 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-mailer 2>/dev/null'
+
+# --hostname gives us a friendlier hostname than the default.
+# The real hostname is passed via a `HOSTNAME` environment variable though.
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-mailer \
+			--log-driver=none \
+			--user={{ matrix_mailer_container_user_uid }}:{{ matrix_mailer_container_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/var/spool/exim:rw,noexec,nosuid,size=100m \
+			--network={{ matrix_docker_network }} \
+			--env-file={{ matrix_mailer_base_path }}/env-mailer \
+			--hostname=matrix-mailer \
+			{% for arg in matrix_mailer_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_mailer_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-mailer 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-mailer 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-mailer
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-nginx-proxy/defaults/main.yml b/roles/matrix-nginx-proxy/defaults/main.yml
new file mode 100644
index 000000000..ba467ad3f
--- /dev/null
+++ b/roles/matrix-nginx-proxy/defaults/main.yml
@@ -0,0 +1,487 @@
+matrix_nginx_proxy_enabled: true
+matrix_nginx_proxy_version: 1.21.1-alpine
+
+# We use an official nginx image, which we fix-up to run unprivileged.
+# An alternative would be an `nginxinc/nginx-unprivileged` image, but
+# that is frequently out of date.
+matrix_nginx_proxy_docker_image: "{{ matrix_container_global_registry_prefix }}nginx:{{ matrix_nginx_proxy_version }}"
+matrix_nginx_proxy_docker_image_force_pull: "{{ matrix_nginx_proxy_docker_image.endswith(':latest') }}"
+
+matrix_nginx_proxy_base_path: "{{ matrix_base_data_path }}/nginx-proxy"
+matrix_nginx_proxy_data_path: "{{ matrix_nginx_proxy_base_path }}/data"
+matrix_nginx_proxy_data_path_in_container: "/nginx-data"
+matrix_nginx_proxy_confd_path: "{{ matrix_nginx_proxy_base_path }}/conf.d"
+
+# List of systemd services that matrix-nginx-proxy.service depends on
+matrix_nginx_proxy_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-nginx-proxy.service wants
+matrix_nginx_proxy_systemd_wanted_services_list: []
+
+# A list of additional "volumes" to mount in the container.
+# This list gets populated dynamically at runtime. You can provide a different default value,
+# if you wish to mount your own files into the container.
+# Contains definition objects like this: `{"src": "/outside", "dst": "/inside", "options": "rw|ro|slave|.."}
+matrix_nginx_proxy_container_additional_volumes: []
+
+# A list of extra arguments to pass to the container
+matrix_nginx_proxy_container_extra_arguments: []
+
+# Controls whether matrix-nginx-proxy serves its vhosts over HTTPS or HTTP.
+#
+# If enabled:
+# - SSL certificates would be expected to be available (see `matrix_ssl_retrieval_method`)
+# - the HTTP vhost would be made a redirect to the HTTPS vhost
+#
+# If not enabled:
+# - you don't need any SSL certificates (you can set `matrix_ssl_retrieval_method: none`)
+# - naturally, there's no HTTPS vhost
+# - services are served directly from the HTTP vhost
+matrix_nginx_proxy_https_enabled: true
+
+# Controls whether the matrix-nginx-proxy container exposes its HTTP port (tcp/8080 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:80"), or empty string to not expose.
+matrix_nginx_proxy_container_http_host_bind_port: '80'
+
+# Controls whether the matrix-nginx-proxy container exposes its HTTPS port (tcp/8443 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:443"), or empty string to not expose.
+#
+# This only makes sense and applies if `matrix_nginx_proxy_https_enabled` is set to `true`.
+# Otherwise, there are no HTTPS vhosts to expose.
+matrix_nginx_proxy_container_https_host_bind_port: '443'
+
+# Controls whether the matrix-nginx-proxy container exposes the Matrix Federation port (tcp/8448 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8448"), or empty string to not expose.
+#
+# This only makes sense and applies if `matrix_nginx_proxy_proxy_matrix_federation_api_enabled` is set to `true`.
+# Otherwise, there is no Matrix Federation port to expose.
+#
+# This port can take HTTP or HTTPS traffic, depending on `matrix_nginx_proxy_https_enabled`.
+# When HTTPS is disabled, you'd likely want to only expose the port locally, and front it with another HTTPS-enabled reverse-proxy.
+matrix_nginx_proxy_container_federation_host_bind_port: '8448'
+
+# Controls whether matrix-nginx-proxy should serve the base domain.
+#
+# This is useful for when you only have your Matrix server, but you need to serve
+# to serve `/.well-known/matrix/*` files from the base domain for the needs of
+# Server-Discovery (Federation) and for Client-Discovery.
+#
+# Besides serving these Matrix files, a homepage would be served with content
+# as specified in the `matrix_nginx_proxy_base_domain_homepage_template` variable.
+# You can also put additional files to use for this webpage
+# in the `{{ matrix_nginx_proxy_data_path }}/matrix-domain` (`/matrix/nginx-proxy/data/matrix-domain`) directory.
+matrix_nginx_proxy_base_domain_serving_enabled: false
+
+matrix_nginx_proxy_base_domain_hostname: "{{ matrix_domain }}"
+
+# Controls whether `matrix_nginx_proxy_base_domain_homepage_template` would be dumped to an `index.html` file
+# in the `/matrix/nginx-proxy/data/matrix-domain` directory.
+#
+# If you would instead like to serve a static website by yourself, you can disable this.
+# When disabled, you're expected to put website files in `/matrix/nginx-proxy/data/matrix-domain` manually
+# and can expect that the playbook won't intefere with the `index.html` file.
+matrix_nginx_proxy_base_domain_homepage_enabled: true
+
+matrix_nginx_proxy_base_domain_homepage_template: |-
+  <!doctype html>
+  <meta charset="utf-8" />
+  <html>
+    <body>
+      Hello from {{ matrix_domain }}!
+    </body>
+  </html>
+
+# Option to disable the access log
+matrix_nginx_proxy_access_log_enabled: true
+
+# Controls whether proxying the riot domain should be done.
+matrix_nginx_proxy_proxy_riot_compat_redirect_enabled: false
+matrix_nginx_proxy_proxy_riot_compat_redirect_hostname: "riot.{{ matrix_domain }}"
+
+# Controls whether proxying the Synapse domain should be done.
+matrix_nginx_proxy_proxy_synapse_enabled: false
+matrix_nginx_proxy_proxy_synapse_hostname: "matrix-nginx-proxy"
+matrix_nginx_proxy_proxy_synapse_federation_api_enabled: "{{ matrix_nginx_proxy_proxy_matrix_federation_api_enabled }}"
+# The addresses where the Federation API is, when using Synapse.
+matrix_nginx_proxy_proxy_synapse_federation_api_addr_with_container: "matrix-synapse:8048"
+matrix_nginx_proxy_proxy_synapse_federation_api_addr_sans_container: "localhost:8048"
+
+# Controls whether proxying the Element domain should be done.
+matrix_nginx_proxy_proxy_element_enabled: false
+matrix_nginx_proxy_proxy_element_hostname: "{{ matrix_server_fqn_element }}"
+
+# Controls whether proxying the Hydrogen domain should be done.
+matrix_nginx_proxy_proxy_hydrogen_enabled: false
+matrix_nginx_proxy_proxy_hydrogen_hostname: "{{ matrix_server_fqn_hydrogen }}"
+
+# Controls whether proxying the matrix domain should be done.
+matrix_nginx_proxy_proxy_matrix_enabled: false
+matrix_nginx_proxy_proxy_matrix_hostname: "{{ matrix_server_fqn_matrix }}"
+# The port name used for federation in the nginx configuration.
+# This is not necessarily the port that it's actually on,
+# as port-mapping happens (`-p ..`) for the `matrix-nginx-proxy` container.
+matrix_nginx_proxy_proxy_matrix_federation_port: 8448
+
+# Controls whether proxying the dimension domain should be done.
+matrix_nginx_proxy_proxy_dimension_enabled: false
+matrix_nginx_proxy_proxy_dimension_hostname: "{{ matrix_server_fqn_dimension }}"
+
+# Controls whether proxying the goneb domain should be done.
+matrix_nginx_proxy_proxy_bot_go_neb_enabled: false
+matrix_nginx_proxy_proxy_bot_go_neb_hostname: "{{ matrix_server_fqn_bot_go_neb }}"
+
+# Controls whether proxying the jitsi domain should be done.
+matrix_nginx_proxy_proxy_jitsi_enabled: false
+matrix_nginx_proxy_proxy_jitsi_hostname: "{{ matrix_server_fqn_jitsi }}"
+
+# Controls whether proxying the grafana domain should be done.
+matrix_nginx_proxy_proxy_grafana_enabled: false
+matrix_nginx_proxy_proxy_grafana_hostname: "{{ matrix_server_fqn_grafana }}"
+
+# Controls whether proxying the sygnal domain should be done.
+matrix_nginx_proxy_proxy_sygnal_enabled: false
+matrix_nginx_proxy_proxy_sygnal_hostname: "{{ matrix_server_fqn_sygnal }}"
+
+# Controls whether proxying for the matrix-corporal API (`/_matrix/corporal`) should be done (on the matrix domain)
+matrix_nginx_proxy_proxy_matrix_corporal_api_enabled: false
+matrix_nginx_proxy_proxy_matrix_corporal_api_addr_with_container: "matrix-corporal:41081"
+matrix_nginx_proxy_proxy_matrix_corporal_api_addr_sans_container: "127.0.0.1:41081"
+
+# Controls whether proxying for the User Directory Search API (`/_matrix/client/r0/user_directory/search`) should be done (on the matrix domain).
+# This can be used to forward the API endpoint to another service, augmenting the functionality of Synapse's own User Directory Search.
+# To learn more, see: https://github.com/ma1uta/ma1sd/blob/master/docs/features/directory.md
+matrix_nginx_proxy_proxy_matrix_user_directory_search_enabled: false
+matrix_nginx_proxy_proxy_matrix_user_directory_search_addr_with_container: "matrix-ma1sd:8090"
+matrix_nginx_proxy_proxy_matrix_user_directory_search_addr_sans_container: "127.0.0.1:8090"
+
+# Controls whether proxying for 3PID-based registration (`/_matrix/client/r0/register/(email|msisdn)/requestToken`) should be done (on the matrix domain).
+# This allows another service to control registrations involving 3PIDs.
+# To learn more, see: https://github.com/ma1uta/ma1sd/blob/master/docs/features/registration.md
+matrix_nginx_proxy_proxy_matrix_3pid_registration_enabled: false
+matrix_nginx_proxy_proxy_matrix_3pid_registration_addr_with_container: "matrix-ma1sd:8090"
+matrix_nginx_proxy_proxy_matrix_3pid_registration_addr_sans_container: "127.0.0.1:8090"
+
+# Controls whether proxying for the Identity API (`/_matrix/identity`) should be done (on the matrix domain)
+matrix_nginx_proxy_proxy_matrix_identity_api_enabled: false
+matrix_nginx_proxy_proxy_matrix_identity_api_addr_with_container: "matrix-ma1sd:8090"
+matrix_nginx_proxy_proxy_matrix_identity_api_addr_sans_container: "127.0.0.1:8090"
+
+# Controls whether proxying for metrics (`/_synapse/metrics`) should be done (on the matrix domain)
+matrix_nginx_proxy_proxy_synapse_metrics: false
+matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_enabled: false
+matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_key: ""
+
+# The addresses where the Matrix Client API is.
+# Certain extensions (like matrix-corporal) may override this in order to capture all traffic.
+matrix_nginx_proxy_proxy_matrix_client_api_addr_with_container: "matrix-nginx-proxy:12080"
+matrix_nginx_proxy_proxy_matrix_client_api_addr_sans_container: "127.0.0.1:12080"
+
+# The addresses where the Matrix Client API is, when using Synapse.
+matrix_nginx_proxy_proxy_synapse_client_api_addr_with_container: "matrix-synapse:8008"
+matrix_nginx_proxy_proxy_synapse_client_api_addr_sans_container: "127.0.0.1:8008"
+
+# This needs to be equal or higher than the maximum upload size accepted by Synapse.
+matrix_nginx_proxy_proxy_matrix_client_api_client_max_body_size_mb: 50
+
+
+# Tells whether `/_synapse/client` is forwarded to the Matrix Client API server.
+matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_client_api_enabled: true
+
+# Tells whether `/_synapse/oidc` is forwarded to the Matrix Client API server.
+# Enable this if you need OpenID Connect authentication support.
+matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_oidc_api_enabled: false
+
+# Tells whether `/_synapse/admin` is forwarded to the Matrix Client API server.
+# Following these recommendations (https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md), by default, we don't.
+matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_admin_api_enabled: false
+
+# `matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_prefixes` holds
+# the location prefixes that get forwarded to the Matrix Client API server.
+# These locations get combined into a regex like this `^(/_matrix|/_synapse/client)`.
+matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_prefix_regexes: |
+  {{
+    (['/_matrix'])
+    +
+    (['/_synapse/client'] if matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_client_api_enabled else [])
+    +
+    (['/_synapse/oidc'] if matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_oidc_api_enabled else [])
+    +
+    (['/_synapse/admin'] if matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_admin_api_enabled else [])
+    +
+    (['/_synapse/metrics'] if matrix_nginx_proxy_proxy_synapse_metrics else [])
+  }}
+
+# Specifies where requests for the root URI (`/`) on the `matrix.` domain should be redirected.
+# If this has an empty value, they're just passed to the homeserver, which serves a static page.
+# If you'd like to make `https://matrix.DOMAIN` redirect to `https://element.DOMAIN` (or something of that sort), specify the domain name here.
+# Example value: `element.DOMAIN` (or `{{ matrix_server_fqn_element }}`).
+matrix_nginx_proxy_proxy_matrix_client_redirect_root_uri_to_domain: ""
+
+# Controls whether proxying for the Matrix Federation API should be done.
+matrix_nginx_proxy_proxy_matrix_federation_api_enabled: false
+matrix_nginx_proxy_proxy_matrix_federation_api_addr_with_container: "matrix-nginx-proxy:12088"
+matrix_nginx_proxy_proxy_matrix_federation_api_addr_sans_container: "localhost:12088"
+matrix_nginx_proxy_proxy_matrix_federation_api_client_max_body_size_mb: "{{ (matrix_nginx_proxy_proxy_matrix_client_api_client_max_body_size_mb | int) * 3 }}"
+matrix_nginx_proxy_proxy_matrix_federation_api_ssl_certificate: "{{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/fullchain.pem"
+matrix_nginx_proxy_proxy_matrix_federation_api_ssl_certificate_key: "{{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/privkey.pem"
+matrix_nginx_proxy_proxy_matrix_federation_api_ssl_trusted_certificate: "{{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/chain.pem"
+
+# The tmpfs at /tmp needs to be large enough to handle multiple concurrent file uploads.
+matrix_nginx_proxy_tmp_directory_size_mb: "{{ (matrix_nginx_proxy_proxy_matrix_federation_api_client_max_body_size_mb | int) * 50 }}"
+
+# A list of strings containing additional configuration blocks to add to the nginx server configuration (nginx.conf).
+# for big matrixservers to enlarge the number of open files to prevent timeouts
+# matrix_nginx_proxy_proxy_additional_configuration_blocks:
+#  - 'worker_rlimit_nofile 30000;'
+matrix_nginx_proxy_proxy_additional_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to the nginx event server configuration (nginx.conf).
+matrix_nginx_proxy_proxy_event_additional_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to the nginx http's server configuration (nginx-http.conf).
+matrix_nginx_proxy_proxy_http_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to the base matrix server configuration (matrix-domain.conf).
+matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to the synapse's server configuration (matrix-synapse.conf).
+matrix_nginx_proxy_proxy_synapse_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Riot's server configuration (matrix-riot-web.conf).
+matrix_nginx_proxy_proxy_riot_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Element's server configuration (matrix-client-element.conf).
+matrix_nginx_proxy_proxy_element_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Element's server configuration (matrix-client-element.conf).
+matrix_nginx_proxy_proxy_hydrogen_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Dimension's server configuration (matrix-dimension.conf).
+matrix_nginx_proxy_proxy_dimension_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to GoNEB's server configuration (matrix-bot-go-neb.conf).
+matrix_nginx_proxy_proxy_bot_go_neb_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Jitsi's server configuration (matrix-jitsi.conf).
+matrix_nginx_proxy_proxy_jitsi_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Grafana's server configuration (matrix-grafana.conf).
+matrix_nginx_proxy_proxy_grafana_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to Sygnal's server configuration (matrix-sygnal.conf).
+matrix_nginx_proxy_proxy_sygnal_additional_server_configuration_blocks: []
+
+# A list of strings containing additional configuration blocks to add to the base domain server configuration (matrix-base-domain.conf).
+matrix_nginx_proxy_proxy_domain_additional_server_configuration_blocks: []
+
+# Controls whether to send a "Permissions-Policy interest-cohort=();" header along with all responses for all vhosts meant to be accessed by users.
+#
+# Learn more about what it is here:
+# - https://www.eff.org/deeplinks/2021/03/googles-floc-terrible-idea
+# - https://paramdeo.com/blog/opting-your-website-out-of-googles-floc-network
+# - https://amifloced.org/
+#
+# Of course, a better solution is to just stop using browsers (like Chrome), which participate in such tracking practices.
+matrix_nginx_proxy_floc_optout_enabled: true
+
+# HSTS Preloading Enable
+#
+# In its strongest and recommended form, the [HSTS policy](https://www.chromium.org/hsts) includes all subdomains, and
+# indicates a willingness to be “preloaded” into browsers:
+# `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
+# For more information visit:
+# - https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
+# - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
+# - https://hstspreload.org/#opt-in
+matrix_nginx_proxy_hsts_preload_enabled: false
+
+# X-XSS-Protection Enable
+# Stops pages from loading when they detect reflected cross-site scripting (XSS) attacks.
+# Note: Not applicable for grafana
+#
+# Learn more about it is here:
+# - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
+# - https://portswigger.net/web-security/cross-site-scripting/reflected
+matrix_nginx_proxy_xss_protection: "1; mode=block"
+
+# Specifies the SSL configuration that should be used for the SSL protocols and ciphers
+# This is based on the Mozilla Server Side TLS Recommended configurations.
+#
+# The posible values are:
+# - "modern" - For Modern clients that support TLS 1.3, with no need for backwards compatibility
+# - "intermediate" - Recommended configuration for a general-purpose server
+# - "old" - Services accessed by very old clients or libraries, such as Internet Explorer 8 (Windows XP), Java 6, or OpenSSL 0.9.8
+#
+# For more information visit:
+# - https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations
+# - https://ssl-config.mozilla.org/#server=nginx
+matrix_nginx_proxy_ssl_preset: "intermediate"
+
+# Presets are taken from Mozilla's Server Side TLS Recommended configurations
+# DO NOT modify these values and use `matrix_nginx_proxy_ssl_protocols`, `matrix_nginx_proxy_ssl_ciphers` and `matrix_nginx_proxy_ssl_ciphers`
+# if you wish to use something more custom.
+matrix_nginx_proxy_ssl_presets:
+  modern:
+    protocols: TLSv1.3
+    ciphers: ""
+    prefer_server_ciphers: "off"
+  intermediate:
+    protocols: TLSv1.2 TLSv1.3
+    ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+    prefer_server_ciphers: "off"
+  old:
+    protocols: TLSv1 TLSv1.1 TLSv1.2 TLSv1.3
+    ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA
+    prefer_server_ciphers: "on"
+
+
+# Specifies which *SSL protocols* to use when serving all the various vhosts.
+matrix_nginx_proxy_ssl_protocols: "{{ matrix_nginx_proxy_ssl_presets[matrix_nginx_proxy_ssl_preset]['protocols'] }}"
+
+# Specifies whether to prefer *the client’s choice or the server’s choice* when negotiating ciphers.
+matrix_nginx_proxy_ssl_prefer_server_ciphers: "{{ matrix_nginx_proxy_ssl_presets[matrix_nginx_proxy_ssl_preset]['prefer_server_ciphers'] }}"
+
+# Specifies which *SSL Cipher suites* to use when serving all the various vhosts.
+# To see the full list for suportes ciphers run `openssl ciphers` on your server
+matrix_nginx_proxy_ssl_ciphers: "{{ matrix_nginx_proxy_ssl_presets[matrix_nginx_proxy_ssl_preset]['ciphers'] }}"
+
+# Controls whether the self-check feature should validate SSL certificates.
+matrix_nginx_proxy_self_check_validate_certificates: true
+
+# Controls whether redirects will be followed when checking the `/.well-known/matrix/client` resource.
+#
+# As per the spec (https://matrix.org/docs/spec/client_server/r0.6.0#well-known-uri), it shouldn't be,
+# so we default to not following redirects as well.
+matrix_nginx_proxy_self_check_well_known_matrix_client_follow_redirects: none
+
+# For OCSP purposes, we need to define a resolver at the `server{}` level or `http{}` level (we do the latter).
+#
+# Otherwise, we get warnings like this:
+# > [warn] 22#22: no resolver defined to resolve r3.o.lencr.org while requesting certificate status, responder: r3.o.lencr.org, certificate: "/matrix/ssl/config/live/.../fullchain.pem"
+#
+# We point it to the internal Docker resolver, which likely delegates to nameservers defined in `/etc/resolv.conf`.
+#
+# When nginx proxy is disabled, our configuration is likely used by non-containerized nginx, so can't use the internal Docker resolver.
+# Pointing `resolver` to some public DNS server might be an option, but for now we impose DNS servers on people.
+# It might also be that no such warnings occur when not running in a container.
+matrix_nginx_proxy_http_level_resolver: "{{ '127.0.0.11' if matrix_nginx_proxy_enabled else '' }}"
+
+# By default, this playbook automatically retrieves and auto-renews
+# free SSL certificates from Let's Encrypt.
+#
+# The following retrieval methods are supported:
+# - "lets-encrypt" - the playbook obtains free SSL certificates from Let's Encrypt
+# - "self-signed" - the playbook generates and self-signs certificates
+# - "manually-managed" - lets you manage certificates by yourself (manually; see below)
+# - "none" - like "manually-managed", but doesn't care if you don't drop certificates in the location it expects
+#
+# If you decide to manage certificates by yourself (`matrix_ssl_retrieval_method: manually-managed`),
+# you'd need to drop them into the directory specified by `matrix_ssl_config_dir_path`
+# obeying the following hierarchy:
+# - <matrix_ssl_config_dir_path>/live/<domain>/fullchain.pem
+# - <matrix_ssl_config_dir_path>/live/<domain>/privkey.pem
+# where <domain> refers to the domains that you need (usually `matrix_server_fqn_matrix` and `matrix_server_fqn_element`).
+#
+# The "none" type (`matrix_ssl_retrieval_method: none`), simply means that no certificate retrieval will happen.
+# It's useful for when you've disabled the nginx proxy (`matrix_nginx_proxy_enabled: false`)
+# and you'll be using another reverse-proxy server (like Apache) with your own certificates, managed by yourself.
+# It's also useful if you're using `matrix_nginx_proxy_https_enabled: false` to make this nginx proxy serve
+# plain HTTP traffic only (usually, on the loopback interface only) and you'd be terminating SSL using another reverse-proxy.
+matrix_ssl_retrieval_method: "lets-encrypt"
+
+matrix_ssl_architecture: "amd64"
+
+# The full list of domains that this role will obtain certificates for.
+# This variable is likely redefined outside of the role, to include the domains that are necessary (depending on the services that are enabled).
+# To add additional domain names, consider using `matrix_ssl_additional_domains_to_obtain_certificates_for` instead.
+matrix_ssl_domains_to_obtain_certificates_for: "{{ matrix_ssl_additional_domains_to_obtain_certificates_for }}"
+
+# A list of additional domain names to obtain certificates for.
+matrix_ssl_additional_domains_to_obtain_certificates_for: []
+
+# Controls whether to obtain production or staging certificates from Let's Encrypt.
+matrix_ssl_lets_encrypt_staging: false
+matrix_ssl_lets_encrypt_certbot_docker_image: "{{ matrix_container_global_registry_prefix }}certbot/certbot:{{ matrix_ssl_architecture }}-v1.17.0"
+matrix_ssl_lets_encrypt_certbot_docker_image_force_pull: "{{ matrix_ssl_lets_encrypt_certbot_docker_image.endswith(':latest') }}"
+matrix_ssl_lets_encrypt_certbot_standalone_http_port: 2402
+matrix_ssl_lets_encrypt_support_email: ~
+
+# Tells which interface and port the Let's Encrypt (certbot) container should try to bind to
+# when it tries to obtain initial certificates in standalone mode.
+#
+# This should normally be a public interface and port.
+# If you'd like to not bind on all IP addresses, specify one explicitly (e.g. `a.b.c.d:80`)
+matrix_ssl_lets_encrypt_container_standalone_http_host_bind_port: '80'
+
+matrix_ssl_base_path: "{{ matrix_base_data_path }}/ssl"
+matrix_ssl_config_dir_path: "{{ matrix_ssl_base_path }}/config"
+matrix_ssl_log_dir_path: "{{ matrix_ssl_base_path }}/log"
+
+# If you'd like to start some service before a certificate is obtained, specify it here.
+# This could be something like `matrix-dynamic-dns`, etc.
+matrix_ssl_pre_obtaining_required_service_name: ~
+matrix_ssl_pre_obtaining_required_service_start_wait_time_seconds: 60
+
+# Nginx Optimize SSL Session
+#
+# ssl_session_cache:
+# - Creating a cache of TLS connection parameters reduces the number of handshakes
+#   and thus can improve the performance of application.
+# - Default session cache is not optimal as it can be used by only one worker process
+#   and can cause memory fragmentation. It is much better to use shared cache.
+# - Learn More: https://nginx.org/en/docs/http/ngx_http_ssl_module.html
+#
+# ssl_session_timeout:
+# - Nginx by default it is set to 5 minutes which is very low.
+#   should be like 4h or 1d but will require you to increase the size of cache.
+# - Learn More:
+#     https://github.com/certbot/certbot/issues/6903
+#     https://github.com/mozilla/server-side-tls/issues/198
+#
+# ssl_session_tickets:
+# - In case of session tickets, information about session is given to the client.
+#   Enabling this improve performance also make Perfect Forward Secrecy useless.
+# - If you would instead like to use ssl_session_tickets by yourself, you can set
+#   matrix_nginx_proxy_ssl_session_tickets_off false.
+# - Learn More: https://github.com/mozilla/server-side-tls/issues/135
+#
+# Presets are taken from Mozilla's Server Side TLS Recommended configurations
+matrix_nginx_proxy_ssl_session_cache: "shared:MozSSL:10m"
+matrix_nginx_proxy_ssl_session_timeout: "1d"
+matrix_nginx_proxy_ssl_session_tickets_off: true
+
+# OCSP Stapling eliminating the need for clients to contact the CA, with the aim of improving both security and performance.
+# OCSP stapling can provide a performance boost of up to 30%
+# nginx web server supports OCSP stapling since version 1.3.7.
+#
+# *warning* Nginx is lazy loading OCSP responses, which means that for the first few web requests it is unable to add the OCSP response.
+# set matrix_nginx_proxy_ocsp_stapling_enabled false to disable OCSP Stapling
+#
+# Learn more about what it is here:
+# - https://en.wikipedia.org/wiki/OCSP_stapling
+# - https://blog.cloudflare.com/high-reliability-ocsp-stapling/
+# - https://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox/
+matrix_nginx_proxy_ocsp_stapling_enabled: true
+
+# nginx status page configurations.
+matrix_nginx_proxy_proxy_matrix_nginx_status_enabled: false
+matrix_nginx_proxy_proxy_matrix_nginx_status_allowed_addresses: ['{{ ansible_default_ipv4.address }}']
+
+
+# synapse worker activation and endpoint mappings
+matrix_nginx_proxy_synapse_workers_enabled: false
+matrix_nginx_proxy_synapse_workers_list: []
+matrix_nginx_proxy_synapse_generic_worker_client_server_locations: []
+matrix_nginx_proxy_synapse_generic_worker_federation_locations: []
+matrix_nginx_proxy_synapse_media_repository_locations: []
+matrix_nginx_proxy_synapse_user_dir_locations: []
+matrix_nginx_proxy_synapse_frontend_proxy_locations: []
+
+# The amount of worker processes and connections
+# Consider increasing these when you are expecting high amounts of traffic
+# http://nginx.org/en/docs/ngx_core_module.html#worker_connections
+matrix_nginx_proxy_worker_processes: 1
+matrix_nginx_proxy_worker_connections: 1024
diff --git a/roles/matrix-nginx-proxy/tasks/init.yml b/roles/matrix-nginx-proxy/tasks/init.yml
new file mode 100644
index 000000000..0161da23f
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/init.yml
@@ -0,0 +1,8 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-nginx-proxy.service'] }}"
+  when: matrix_nginx_proxy_enabled|bool
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + [item.name] }}"
+  when: "item.applicable|bool and item.enableable|bool"
+  with_items: "{{ matrix_ssl_renewal_systemd_units_list }}"
diff --git a/roles/matrix-nginx-proxy/tasks/main.yml b/roles/matrix-nginx-proxy/tasks/main.yml
new file mode 100644
index 000000000..ad1119511
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/main.yml
@@ -0,0 +1,38 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+# Always validating the configuration, even if `matrix_nginx_proxy: false`.
+# This role performs actions even if the role is disabled, so we need
+# to ensure there's a valid configuration in any case.
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-nginx-proxy
+
+- import_tasks: "{{ role_path }}/tasks/ssl/main.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-nginx-proxy
+    - setup-ssl
+
+- import_tasks: "{{ role_path }}/tasks/setup_nginx_proxy.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-nginx-proxy
+
+- import_tasks: "{{ role_path }}/tasks/self_check_well_known.yml"
+  delegate_to: 127.0.0.1
+  become: false
+  when: run_self_check|bool
+  tags:
+    - self-check
+
+- name: Mark matrix-nginx-proxy role as executed
+  set_fact:
+    matrix_nginx_proxy_role_executed: true
+  tags:
+   - always
diff --git a/roles/matrix-nginx-proxy/tasks/self_check_well_known.yml b/roles/matrix-nginx-proxy/tasks/self_check_well_known.yml
new file mode 100644
index 000000000..be1b65553
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/self_check_well_known.yml
@@ -0,0 +1,30 @@
+---
+
+- name: Determine well-known files to check (Matrix)
+  set_fact:
+    well_known_file_checks:
+      - path: /.well-known/matrix/client
+        purpose: Client Discovery
+        cors: true
+        follow_redirects: "{{ matrix_nginx_proxy_self_check_well_known_matrix_client_follow_redirects }}"
+        validate_certs: "{{ matrix_nginx_proxy_self_check_validate_certificates }}"
+
+- block:
+    - set_fact:
+        well_known_file_check_matrix_server:
+          path: /.well-known/matrix/server
+          purpose: Server Discovery
+          cors: false
+          follow_redirects: safe
+          validate_certs: "{{ matrix_nginx_proxy_self_check_validate_certificates }}"
+
+    - name: Determine domains that we require certificates for (ma1sd)
+      set_fact:
+        well_known_file_checks: "{{ well_known_file_checks + [well_known_file_check_matrix_server] }}"
+  when: matrix_well_known_matrix_server_enabled|bool
+
+- name: Perform well-known checks
+  include_tasks: "{{ role_path }}/tasks/self_check_well_known_file.yml"
+  with_items: "{{ well_known_file_checks }}"
+  loop_control:
+    loop_var: well_known_file_check
diff --git a/roles/matrix-nginx-proxy/tasks/self_check_well_known_file.yml b/roles/matrix-nginx-proxy/tasks/self_check_well_known_file.yml
new file mode 100644
index 000000000..6f831a290
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/self_check_well_known_file.yml
@@ -0,0 +1,73 @@
+---
+
+- set_fact:
+    well_known_url_matrix: "https://{{ matrix_server_fqn_matrix }}{{ well_known_file_check.path }}"
+    well_known_url_identity: "https://{{ matrix_domain }}{{ well_known_file_check.path }}"
+
+# These well-known files may be served without a `Content-Type: application/json` header,
+# so we can't rely on the uri module's automatic parsing of JSON.
+- name: Check .well-known on the matrix hostname
+  uri:
+    url: "{{ well_known_url_matrix }}"
+    follow_redirects: none
+    return_content: true
+    validate_certs: "{{ well_known_file_check.validate_certs }}"
+    headers:
+      Origin: example.com
+  check_mode: no
+  register: result_well_known_matrix
+  ignore_errors: true
+
+- name: Fail if .well-known not working on the matrix hostname
+  fail:
+    msg: "Failed checking that the well-known file for {{ well_known_file_check.purpose }} is configured at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ well_known_url_matrix }}`). Is port 443 open in your firewall? Full error: {{ result_well_known_matrix }}"
+  when: "result_well_known_matrix.failed"
+
+- name: Parse JSON for well-known payload at the matrix hostname
+  set_fact:
+    well_known_matrix_payload: "{{ result_well_known_matrix.content|from_json }}"
+
+- name: Fail if .well-known not CORS-aware on the matrix hostname
+  fail:
+    msg: "The well-known file for {{ well_known_file_check.purpose }} on `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ well_known_url_matrix }}`) is not CORS-aware. The file needs to be served with an Access-Control-Allow-Origin header set."
+  when: "well_known_file_check.cors and 'access_control_allow_origin' not in result_well_known_matrix"
+
+- name: Report working .well-known on the matrix hostname
+  debug:
+    msg: "well-known for {{ well_known_file_check.purpose }} is configured correctly for `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ well_known_url_matrix }}`)"
+
+- name: Check .well-known on the identity hostname
+  uri:
+    url: "{{ well_known_url_identity }}"
+    follow_redirects: "{{ well_known_file_check.follow_redirects }}"
+    return_content: true
+    validate_certs: "{{ well_known_file_check.validate_certs }}"
+    headers:
+      Origin: example.com
+  check_mode: no
+  register: result_well_known_identity
+  ignore_errors: true
+
+- name: Fail if .well-known not working on the identity hostname
+  fail:
+    msg: "Failed checking that the well-known file for {{ well_known_file_check.purpose }} is configured at `{{ matrix_domain }}` (checked endpoint: `{{ well_known_url_identity }}`). Is port 443 open in your firewall? Full error: {{ result_well_known_identity }}"
+  when: "result_well_known_identity.failed"
+
+- name: Parse JSON for well-known payload at the identity hostname
+  set_fact:
+    well_known_identity_payload: "{{ result_well_known_identity.content|from_json }}"
+
+- name: Fail if .well-known not CORS-aware on the identity hostname
+  fail:
+    msg: "The well-known file for {{ well_known_file_check.purpose }} on `{{ matrix_domain }}` (checked endpoint: `{{ well_known_url_identity }}`) is not CORS-aware. The file needs to be served with an Access-Control-Allow-Origin header set. See docs/configuring-well-known.md"
+  when: "well_known_file_check.cors and 'access_control_allow_origin' not in result_well_known_identity"
+
+# For people who manually copy the well-known file, try to detect if it's outdated
+- name: Fail if well-known is different on matrix hostname and identity hostname
+  fail:
+    msg: "The well-known files for {{ well_known_file_check.purpose }} at `{{ matrix_server_fqn_matrix }}` and `{{ matrix_domain }}` are different. Perhaps you copied the file ({{ well_known_file_check.path }}) manually before and now it's outdated?"
+  when: "well_known_matrix_payload != well_known_identity_payload"
+
+- name: Report working .well-known on the identity hostname
+  debug:
+    msg: "well-known for {{ well_known_file_check.purpose }} ({{ well_known_file_check.path }}) is configured correctly for `{{ matrix_domain }}` (checked endpoint: `{{ well_known_url_identity }}`)"
diff --git a/roles/matrix-nginx-proxy/tasks/setup_nginx_proxy.yml b/roles/matrix-nginx-proxy/tasks/setup_nginx_proxy.yml
new file mode 100644
index 000000000..1d59f5677
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/setup_nginx_proxy.yml
@@ -0,0 +1,272 @@
+---
+
+#
+# Generic tasks that we always want to happen, regardless
+# if the user wants matrix-nginx-proxy or not.
+#
+# If the user would set up their own nginx proxy server,
+# the config files from matrix-nginx-proxy can be reused.
+#
+# It doesn't hurt to put them in place, even if they turn out
+# to be unnecessary.
+#
+- name: Ensure Matrix nginx-proxy paths exist
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_nginx_proxy_base_path }}"
+    - "{{ matrix_nginx_proxy_data_path }}"
+    - "{{ matrix_nginx_proxy_confd_path }}"
+
+- name: Ensure Matrix nginx-proxy configured (main config override)
+  template:
+    src: "{{ role_path }}/templates/nginx/nginx.conf.j2"
+    dest: "{{ matrix_nginx_proxy_base_path }}/nginx.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_enabled|bool
+
+- name: Ensure matrix-synapse-metrics-htpasswd is present (protecting /_synapse/metrics URI)
+  template:
+    src: "{{ role_path }}/templates/nginx/matrix-synapse-metrics-htpasswd.j2"
+    dest: "{{ matrix_nginx_proxy_data_path }}/matrix-synapse-metrics-htpasswd"
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+    mode: 0400
+  when: "matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_enabled|bool and matrix_nginx_proxy_proxy_synapse_metrics|bool"
+
+- name: Ensure Matrix nginx-proxy configured (generic)
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/nginx-http.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/nginx-http.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for matrix-synapse exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-synapse.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-synapse.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_synapse_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for matrix-synapse deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-synapse.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_synapse_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for Element domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-client-element.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-client-element.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_element_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for riot domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-riot-web.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-riot-web.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_riot_compat_redirect_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for Hydrogen domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-client-hydrogen.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-client-hydrogen.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_hydrogen_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for dimension domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-dimension.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-dimension.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_dimension_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for goneb domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-bot-go-neb.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-bot-go-neb.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_bot_go_neb_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for jitsi domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-jitsi.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-jitsi.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_jitsi_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for grafana domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-grafana.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-grafana.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_grafana_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for sygnal domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-sygnal.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-sygnal.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_proxy_sygnal_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for Matrix domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-domain.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-domain.conf"
+    mode: 0644
+
+- name: Ensure Matrix nginx-proxy data directory for base domain exists
+  file:
+    path: "{{ matrix_nginx_proxy_data_path }}/matrix-domain"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: matrix_nginx_proxy_base_domain_serving_enabled|bool
+
+- name: Ensure Matrix nginx-proxy homepage for base domain exists
+  copy:
+    content: "{{ matrix_nginx_proxy_base_domain_homepage_template }}"
+    dest: "{{ matrix_nginx_proxy_data_path }}/matrix-domain/index.html"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: matrix_nginx_proxy_base_domain_serving_enabled|bool and matrix_nginx_proxy_base_domain_homepage_enabled|bool
+
+- name: Ensure Matrix nginx-proxy configuration for base domain exists
+  template:
+    src: "{{ role_path }}/templates/nginx/conf.d/matrix-base-domain.conf.j2"
+    dest: "{{ matrix_nginx_proxy_confd_path }}/matrix-base-domain.conf"
+    mode: 0644
+  when: matrix_nginx_proxy_base_domain_serving_enabled|bool
+
+#
+# Tasks related to setting up matrix-nginx-proxy
+#
+- name: Ensure nginx Docker image is pulled
+  docker_image:
+    name: "{{ matrix_nginx_proxy_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_nginx_proxy_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_nginx_proxy_docker_image_force_pull }}"
+  when: matrix_nginx_proxy_enabled|bool
+
+- name: Ensure matrix-nginx-proxy.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-nginx-proxy.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-nginx-proxy.service"
+    mode: 0644
+  register: matrix_nginx_proxy_systemd_service_result
+  when: matrix_nginx_proxy_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-nginx-proxy.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_nginx_proxy_enabled and matrix_nginx_proxy_systemd_service_result.changed"
+
+
+#
+# Tasks related to getting rid of matrix-nginx-proxy (if it was previously enabled)
+#
+
+- name: Check existence of matrix-nginx-proxy service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-nginx-proxy.service"
+  register: matrix_nginx_proxy_service_stat
+  when: "not matrix_nginx_proxy_enabled|bool"
+
+- name: Ensure matrix-nginx-proxy is stopped
+  service:
+    name: matrix-nginx-proxy
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_nginx_proxy_enabled|bool and matrix_nginx_proxy_service_stat.stat.exists"
+
+- name: Ensure matrix-nginx-proxy.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-nginx-proxy.service"
+    state: absent
+  when: "not matrix_nginx_proxy_enabled|bool and matrix_nginx_proxy_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-nginx-proxy.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_nginx_proxy_enabled|bool and matrix_nginx_proxy_service_stat.stat.exists"
+
+- name: Ensure Matrix nginx-proxy configuration for matrix domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-domain.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_matrix_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for riot domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-riot-web.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_riot_compat_redirect_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for Hydrogen domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-client-hydrogen.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_hydrogen_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for dimension domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-dimension.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_dimension_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for goneb domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-bot-go-neb.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_bot_go_neb_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for jitsi domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-jitsi.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_jitsi_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for grafana domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-grafana.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_grafana_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for sygnal domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-sygnal.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_sygnal_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy homepage for base domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_data_path }}/matrix-domain/index.html"
+    state: absent
+  when: "not matrix_nginx_proxy_base_domain_serving_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for base domain deleted
+  file:
+    path: "{{ matrix_nginx_proxy_confd_path }}/matrix-base-domain.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_base_domain_serving_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy configuration for main config override deleted
+  file:
+    path: "{{ matrix_nginx_proxy_base_path }}/nginx.conf"
+    state: absent
+  when: "not matrix_nginx_proxy_enabled|bool"
+
+- name: Ensure Matrix nginx-proxy htpasswd is deleted (protecting /_synapse/metrics URI)
+  file:
+    path: "{{ matrix_nginx_proxy_data_path }}/matrix-synapse-metrics-htpasswd"
+    state: absent
+  when: "not matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_enabled|bool or not matrix_nginx_proxy_proxy_synapse_metrics|bool"
diff --git a/roles/matrix-nginx-proxy/tasks/setup_well_known.yml b/roles/matrix-nginx-proxy/tasks/setup_well_known.yml
new file mode 100644
index 000000000..3e43a8c60
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/setup_well_known.yml
@@ -0,0 +1,24 @@
+- set_fact:
+    matrix_well_known_file_path: "{{ matrix_static_files_base_path }}/.well-known/matrix/client"
+
+# We need others to be able to read these directories too,
+# so that matrix-nginx-proxy's nginx user can access the files.
+#
+# For running with another webserver, we recommend being part of the `matrix` group.
+- name: Ensure Matrix static-files path exists
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0755
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_static_files_base_path }}/.well-known/matrix"
+
+- name: Ensure Matrix /.well-known/matrix/client configured
+  template:
+    src: "{{ role_path }}/templates/well-known/matrix-client.j2"
+    dest: "{{ matrix_static_files_base_path }}/.well-known/matrix"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
\ No newline at end of file
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/main.yml b/roles/matrix-nginx-proxy/tasks/ssl/main.yml
new file mode 100644
index 000000000..6c0608186
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/main.yml
@@ -0,0 +1,31 @@
+---
+
+- name: Fail if using unsupported SSL certificate retrieval method
+  fail:
+    msg: "The `matrix_ssl_retrieval_method` variable contains an unsupported value"
+  when: "matrix_ssl_retrieval_method not in ['lets-encrypt', 'self-signed', 'manually-managed', 'none']"
+
+
+# Common tasks, required by almost any method below.
+
+- name: Ensure SSL certificate paths exists
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0770
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+    recurse: true
+  with_items:
+    - "{{ matrix_ssl_log_dir_path }}"
+    - "{{ matrix_ssl_config_dir_path }}"
+  when: "matrix_ssl_retrieval_method != 'none'"
+
+
+# Method specific tasks follow
+
+- import_tasks: tasks/ssl/setup_ssl_lets_encrypt.yml
+
+- import_tasks: tasks/ssl/setup_ssl_self_signed.yml
+
+- import_tasks: tasks/ssl/setup_ssl_manually_managed.yml
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt.yml
new file mode 100644
index 000000000..bfd25894a
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt.yml
@@ -0,0 +1,64 @@
+---
+
+# This is a cleanup/migration task, because of to the new way we manage cronjobs (`cron` module) and the new script name.
+# This migration task can be removed some time in the future.
+- name: (Migration) Remove deprecated Let's Encrypt SSL certificate management files
+  file:
+    path: "{{ item }}"
+    state: absent
+  with_items:
+    - "{{ matrix_local_bin_path }}/matrix-ssl-certificates-renew"
+    - "{{ matrix_cron_path }}/matrix-ssl-certificate-renewal"
+    - "{{ matrix_cron_path }}/matrix-nginx-proxy-periodic-restarter"
+    - "/etc/cron.d/matrix-ssl-lets-encrypt"
+
+#
+# Tasks related to setting up Let's Encrypt's management of certificates
+#
+
+- block:
+    - name: Ensure certbot Docker image is pulled
+      docker_image:
+        name: "{{ matrix_ssl_lets_encrypt_certbot_docker_image }}"
+        source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+        force_source: "{{ matrix_ssl_lets_encrypt_certbot_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+        force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_ssl_lets_encrypt_certbot_docker_image_force_pull }}"
+
+    - name: Obtain Let's Encrypt certificates
+      include_tasks: "{{ role_path }}/tasks/ssl/setup_ssl_lets_encrypt_obtain_for_domain.yml"
+      with_items: "{{ matrix_ssl_domains_to_obtain_certificates_for }}"
+      loop_control:
+        loop_var: domain_name
+
+    - name: Ensure Let's Encrypt SSL renewal script installed
+      template:
+        src: "{{ role_path }}/templates/usr-local-bin/matrix-ssl-lets-encrypt-certificates-renew.j2"
+        dest: "{{ matrix_local_bin_path }}/matrix-ssl-lets-encrypt-certificates-renew"
+        mode: 0755
+
+    - name: Ensure SSL renewal systemd units installed
+      template:
+        src: "{{ role_path }}/templates/systemd/{{ item.name }}.j2"
+        dest: "{{ matrix_systemd_path }}/{{ item.name }}"
+        mode: 0644
+      when: "item.applicable|bool"
+      with_items: "{{ matrix_ssl_renewal_systemd_units_list }}"
+  when: "matrix_ssl_retrieval_method == 'lets-encrypt'"
+
+#
+# Tasks related to getting rid of Let's Encrypt's management of certificates
+#
+
+- block:
+    - name: Ensure matrix-ssl-lets-encrypt-renew cronjob removed
+      file:
+        path: "{{ matrix_systemd_path }}/{{ item.name }}"
+        state: absent
+      when: "not item.applicable|bool"
+      with_items: "{{ matrix_ssl_renewal_systemd_units_list }}"
+
+    - name: Ensure Let's Encrypt SSL renewal script removed
+      file:
+        path: "{{ matrix_local_bin_path }}/matrix-ssl-lets-encrypt-certificates-renew"
+        state: absent
+  when: "matrix_ssl_retrieval_method != 'lets-encrypt'"
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt_obtain_for_domain.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt_obtain_for_domain.yml
new file mode 100644
index 000000000..4639f122c
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_lets_encrypt_obtain_for_domain.yml
@@ -0,0 +1,91 @@
+- debug:
+    msg: "Dealing with SSL certificate retrieval for domain: {{ domain_name }}"
+
+- set_fact:
+    domain_name_certificate_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/fullchain.pem"
+
+- name: Check if a certificate for the domain already exists
+  stat:
+    path: "{{ domain_name_certificate_path }}"
+  register: domain_name_certificate_path_stat
+
+- set_fact:
+    domain_name_needs_cert: "{{ not domain_name_certificate_path_stat.stat.exists }}"
+
+- block:
+  - name: Ensure required service for obtaining is started
+    service:
+      name: "{{ matrix_ssl_pre_obtaining_required_service_name }}"
+      state: started
+    register: matrix_ssl_pre_obtaining_required_service_start_result
+
+  - name: Wait some time, so that the required service for obtaining can start
+    wait_for:
+      timeout: "{{ matrix_ssl_service_to_start_before_obtaining_start_wait_time_seconds }}"
+    when: "matrix_ssl_pre_obtaining_required_service_start_result.changed|bool"
+  when: "domain_name_needs_cert|bool and matrix_ssl_pre_obtaining_required_service_name != ''"
+
+# This will fail if there is something running on port 80 (like matrix-nginx-proxy).
+# We suppress the error, as we'll try another method below.
+- name: Attempt initial SSL certificate retrieval with standalone authenticator (directly)
+  shell: >-
+    {{ matrix_host_command_docker }} run
+    --rm
+    --name=matrix-certbot
+    --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+    --cap-drop=ALL
+    -p {{ matrix_ssl_lets_encrypt_container_standalone_http_host_bind_port }}:8080
+    --mount type=bind,src={{ matrix_ssl_config_dir_path }},dst=/etc/letsencrypt
+    --mount type=bind,src={{ matrix_ssl_log_dir_path }},dst=/var/log/letsencrypt
+    {{ matrix_ssl_lets_encrypt_certbot_docker_image }}
+    certonly
+    --non-interactive
+    --work-dir=/tmp
+    --http-01-port 8080
+    {% if matrix_ssl_lets_encrypt_staging %}--staging{% endif %}
+    --standalone
+    --preferred-challenges http
+    --agree-tos
+    --email={{ matrix_ssl_lets_encrypt_support_email }}
+    -d {{ domain_name }}
+  when: domain_name_needs_cert|bool
+  register: result_certbot_direct
+  ignore_errors: true
+
+# If matrix-nginx-proxy is configured from a previous run of this playbook,
+# and it's running now, it may be able to proxy requests to `matrix_ssl_lets_encrypt_certbot_standalone_http_port`.
+- name: Attempt initial SSL certificate retrieval with standalone authenticator (via proxy)
+  shell: >-
+    {{ matrix_host_command_docker }} run
+    --rm
+    --name=matrix-certbot
+    --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+    --cap-drop=ALL
+    -p 127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }}:8080
+    --network={{ matrix_docker_network }}
+    --mount type=bind,src={{ matrix_ssl_config_dir_path }},dst=/etc/letsencrypt
+    --mount type=bind,src={{ matrix_ssl_log_dir_path }},dst=/var/log/letsencrypt
+    {{ matrix_ssl_lets_encrypt_certbot_docker_image }}
+    certonly
+    --non-interactive
+    --work-dir=/tmp
+    --http-01-port 8080
+    {% if matrix_ssl_lets_encrypt_staging %}--staging{% endif %}
+    --standalone
+    --preferred-challenges http
+    --agree-tos
+    --email={{ matrix_ssl_lets_encrypt_support_email }}
+    -d {{ domain_name }}
+  when: "domain_name_needs_cert and result_certbot_direct.failed"
+  register: result_certbot_proxy
+  ignore_errors: true
+
+- name: Fail if all SSL certificate retrieval attempts failed
+  fail:
+    msg: |
+      Failed to obtain a certificate directly (by listening on port 80)
+      and also failed to obtain by relying on the server at port 80 to proxy the request.
+      See above for details.
+      You may wish to set up proxying of /.well-known/acme-challenge to {{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }} or,
+      more easily, stop the server on port 80 while this playbook runs.
+  when: "domain_name_needs_cert and result_certbot_direct.failed and result_certbot_proxy.failed"
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed.yml
new file mode 100644
index 000000000..ea39f5e9d
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed.yml
@@ -0,0 +1,8 @@
+---
+
+- name: Verify certificates
+  include_tasks: "{{ role_path }}/tasks/ssl/setup_ssl_manually_managed_verify_for_domain.yml"
+  with_items: "{{ matrix_ssl_domains_to_obtain_certificates_for }}"
+  loop_control:
+    loop_var: domain_name
+  when: "matrix_ssl_retrieval_method == 'manually-managed'"
\ No newline at end of file
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed_verify_for_domain.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed_verify_for_domain.yml
new file mode 100644
index 000000000..be0444b13
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_manually_managed_verify_for_domain.yml
@@ -0,0 +1,23 @@
+---
+
+- set_fact:
+    matrix_ssl_certificate_verification_cert_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/fullchain.pem"
+    matrix_ssl_certificate_verification_cert_key_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/privkey.pem"
+
+- name: Check if SSL certificate file exists
+  stat:
+    path: "{{ matrix_ssl_certificate_verification_cert_path }}"
+  register: matrix_ssl_certificate_verification_cert_path_stat_result
+
+- fail:
+    msg: "Failed finding a certificate file (for domain `{{ domain_name }}`) at `{{ matrix_ssl_certificate_verification_cert_path }}`"
+  when: "not matrix_ssl_certificate_verification_cert_path_stat_result.stat.exists"
+
+- name: Check if SSL certificate key file exists
+  stat:
+    path: "{{ matrix_ssl_certificate_verification_cert_key_path }}"
+  register: matrix_ssl_certificate_verification_cert_key_path_stat_result
+
+- fail:
+    msg: "Failed finding a certificate key file (for domain `{{ domain_name }}`) at `{{ matrix_ssl_certificate_verification_cert_key_path }}`"
+  when: "not matrix_ssl_certificate_verification_cert_key_path_stat_result.stat.exists"
\ No newline at end of file
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed.yml
new file mode 100644
index 000000000..8fa316da0
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed.yml
@@ -0,0 +1,32 @@
+---
+
+- name: Ensure OpenSSL installed (RedHat)
+  yum:
+    name:
+      - openssl
+    state: present
+    update_cache: no
+  when: "matrix_ssl_retrieval_method == 'self-signed' and ansible_os_family == 'RedHat'"
+
+- name: Ensure APT usage dependencies are installed (Debian)
+  apt:
+    name:
+      - openssl
+    state: present
+    update_cache: no
+  when: "matrix_ssl_retrieval_method == 'self-signed' and ansible_os_family == 'Debian'"
+
+- name: Ensure OpenSSL installed (Archlinux)
+  pacman:
+    name:
+      - openssl
+    state: latest
+    update_cache: no
+  when: "matrix_ssl_retrieval_method == 'self-signed' and ansible_distribution == 'Archlinux'"
+
+- name: Generate self-signed certificates
+  include_tasks: "{{ role_path }}/tasks/ssl/setup_ssl_self_signed_obtain_for_domain.yml"
+  with_items: "{{ matrix_ssl_domains_to_obtain_certificates_for }}"
+  loop_control:
+    loop_var: domain_name
+  when: "matrix_ssl_retrieval_method == 'self-signed'"
diff --git a/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed_obtain_for_domain.yml b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed_obtain_for_domain.yml
new file mode 100644
index 000000000..aea17cc02
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/ssl/setup_ssl_self_signed_obtain_for_domain.yml
@@ -0,0 +1,42 @@
+---
+
+- set_fact:
+    matrix_ssl_certificate_csr_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/csr.csr"
+    matrix_ssl_certificate_cert_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/fullchain.pem"
+    matrix_ssl_certificate_cert_key_path: "{{ matrix_ssl_config_dir_path }}/live/{{ domain_name }}/privkey.pem"
+
+- name: Check if SSL certificate file exists
+  stat:
+    path: "{{ matrix_ssl_certificate_cert_path }}"
+  register: matrix_ssl_certificate_cert_path_stat_result
+
+# In order to do any sort of generation (below), we need to ensure the directory exists first
+- name: Ensure SSL certificate directory exists
+  file:
+    path: "{{ matrix_ssl_certificate_csr_path|dirname }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "not matrix_ssl_certificate_cert_path_stat_result.stat.exists"
+
+# The proper way to do this is by using a sequence of
+# `openssl_privatekey`, `openssl_csr` and `openssl_certificate`.
+#
+# Unfortunately, `openssl_csr` and `openssl_certificate` require `PyOpenSSL>=0.15` to work,
+# which is not available on CentOS 7 (at least).
+#
+# We'll do it in a more manual way.
+- name: Generate SSL certificate
+  command: |
+    openssl req -x509 \
+    -sha256 \
+    -newkey rsa:4096 \
+    -nodes \
+    -subj "/CN={{ domain_name }}" \
+    -keyout {{ matrix_ssl_certificate_cert_key_path }} \
+    -out {{ matrix_ssl_certificate_cert_path }} \
+    -days 3650
+  become: true
+  become_user: "{{ matrix_user_username }}"
+  when: "not matrix_ssl_certificate_cert_path_stat_result.stat.exists"
diff --git a/roles/matrix-nginx-proxy/tasks/validate_config.yml b/roles/matrix-nginx-proxy/tasks/validate_config.yml
new file mode 100644
index 000000000..9661ae5e9
--- /dev/null
+++ b/roles/matrix-nginx-proxy/tasks/validate_config.yml
@@ -0,0 +1,47 @@
+---
+
+- name: (Deprecation) Catch and report renamed settings
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in vars"
+  with_items:
+    - {'old': 'matrix_nginx_proxy_matrix_client_api_addr_with_proxy_container', 'new': 'matrix_nginx_proxy_proxy_matrix_client_api_addr_with_container'}
+    - {'old': 'matrix_nginx_proxy_matrix_client_api_addr_sans_proxy_container', 'new': 'matrix_nginx_proxy_proxy_matrix_client_api_addr_sans_container'}
+    # People who configured this to disable Riot, would now wish to be disabling Element.
+    # We now also have `matrix_nginx_proxy_proxy_riot_compat_redirect_`, but that's something else and is disabled by default.
+    - {'old': 'matrix_nginx_proxy_proxy_riot_enabled', 'new': 'matrix_nginx_proxy_proxy_element_enabled'}
+    - {'old': 'matrix_ssl_lets_encrypt_renew_cron_time_definition', 'new': '<not configurable anymore>'}
+    - {'old': 'matrix_nginx_proxy_reload_cron_time_definition', 'new': '<not configurable anymore>'}
+
+- name: Fail on unknown matrix_ssl_retrieval_method
+  fail:
+    msg: >-
+      `matrix_ssl_retrieval_method` needs to be set to a known value.
+  when: "matrix_ssl_retrieval_method not in ['lets-encrypt', 'self-signed', 'manually-managed', 'none']"
+
+- name: Fail on unknown matrix_nginx_proxy_ssl_config
+  fail:
+    msg: >-
+      `matrix_nginx_proxy_ssl_preset` needs to be set to a known value.
+  when: "matrix_nginx_proxy_ssl_preset not in ['modern', 'intermediate', 'old']"
+
+- block:
+    - name: (Deprecation) Catch and report renamed settings
+      fail:
+        msg: >-
+          Your configuration contains a variable, which now has a different name.
+          Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+      with_items:
+        - {'old': 'host_specific_matrix_ssl_support_email', 'new': 'matrix_ssl_lets_encrypt_support_email'}
+        - {'old': 'host_specific_matrix_ssl_lets_encrypt_support_email', 'new': 'matrix_ssl_lets_encrypt_support_email'}
+      when: "item.old in vars"
+
+    - name: Fail if required variables are undefined
+      fail:
+        msg: "The `{{ item }}` variable must be defined and have a non-null value"
+      with_items:
+        - "matrix_ssl_lets_encrypt_support_email"
+      when: "vars[item] == '' or vars[item] is none"
+  when: "matrix_ssl_retrieval_method == 'lets-encrypt'"
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-base-domain.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-base-domain.conf.j2
new file mode 100644
index 000000000..37863d738
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-base-domain.conf.j2
@@ -0,0 +1,95 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	root /nginx-data/matrix-domain;
+
+	gzip on;
+	gzip_types text/plain application/json;
+
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_domain_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	location /.well-known/matrix {
+		root {{ matrix_static_files_base_path }};
+		{#
+			A somewhat long expires value is used to prevent outages
+			in case this is unreachable due to network failure.
+		#}
+		expires 4h;
+		default_type application/json;
+		add_header Access-Control-Allow-Origin *;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+
+	server_name {{ matrix_nginx_proxy_base_domain_hostname }};
+	server_tokens off;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_base_domain_hostname }};
+	server_tokens off;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_base_domain_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_base_domain_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_base_domain_hostname }}/chain.pem;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-bot-go-neb.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-bot-go-neb.conf.j2
new file mode 100644
index 000000000..6cb5f57a1
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-bot-go-neb.conf.j2
@@ -0,0 +1,95 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Content-Type-Options nosniff;
+
+{% for configuration_block in matrix_nginx_proxy_proxy_bot_go_neb_additional_server_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-bot-go-neb:4050";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:4050;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+	server_name {{ matrix_nginx_proxy_proxy_bot_go_neb_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_bot_go_neb_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_bot_go_neb_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_bot_go_neb_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_bot_go_neb_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-element.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-element.conf.j2
new file mode 100644
index 000000000..2f4f4aa15
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-element.conf.j2
@@ -0,0 +1,104 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-Content-Type-Options nosniff;
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header Content-Security-Policy "frame-ancestors 'self'";
+
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_element_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-client-element:8080";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:8765;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+
+	server_name {{ matrix_nginx_proxy_proxy_element_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_element_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_element_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_element_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != "" %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_element_hostname }}/chain.pem;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-hydrogen.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-hydrogen.conf.j2
new file mode 100644
index 000000000..d9a05926c
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-client-hydrogen.conf.j2
@@ -0,0 +1,102 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Content-Type-Options nosniff;
+	add_header X-Frame-Options SAMEORIGIN;
+	add_header Content-Security-Policy "frame-ancestors 'none'";
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_hydrogen_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-client-hydrogen:8080";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:8768;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+
+	server_name {{ matrix_nginx_proxy_proxy_hydrogen_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_hydrogen_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_hydrogen_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_hydrogen_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != "" %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_element_hostname }}/chain.pem;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-dimension.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-dimension.conf.j2
new file mode 100644
index 000000000..ef8ee972d
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-dimension.conf.j2
@@ -0,0 +1,98 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Content-Type-Options nosniff;
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+{% for configuration_block in matrix_nginx_proxy_proxy_dimension_additional_server_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-dimension:8184";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:8184;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+	server_name {{ matrix_nginx_proxy_proxy_dimension_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_dimension_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_dimension_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_dimension_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_dimension_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-domain.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-domain.conf.j2
new file mode 100644
index 000000000..7b26434d9
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-domain.conf.j2
@@ -0,0 +1,293 @@
+#jinja2: lstrip_blocks: "True"
+{% macro render_nginx_status_location_block(addresses) %}
+	{# Empty first line to make indentation prettier. #}
+
+	location /nginx_status {
+		stub_status on;
+		access_log off;
+		{% for address in addresses %}
+		allow {{ address }};
+		{% endfor %}
+		deny all;
+	}
+{% endmacro %}
+
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json;
+
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+
+	location /.well-known/matrix {
+		root {{ matrix_static_files_base_path }};
+		{#
+			A somewhat long expires value is used to prevent outages
+			in case this is unreachable due to network failure or
+			due to the base domain's server completely dying.
+		#}
+		expires 4h;
+		default_type application/json;
+		add_header Access-Control-Allow-Origin *;
+	}
+
+	{% if matrix_nginx_proxy_proxy_matrix_nginx_status_enabled %}
+		{{ render_nginx_status_location_block(matrix_nginx_proxy_proxy_matrix_nginx_status_allowed_addresses) }}
+	{% endif %}
+
+	{% if matrix_nginx_proxy_proxy_matrix_corporal_api_enabled %}
+	location ^~ /_matrix/corporal {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_corporal_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_corporal_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+	}
+	{% endif %}
+
+	{% if matrix_nginx_proxy_proxy_matrix_identity_api_enabled %}
+	location ^~ /_matrix/identity {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_identity_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_identity_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+	}
+	{% endif %}
+
+	{% if matrix_nginx_proxy_proxy_matrix_user_directory_search_enabled %}
+	location ^~ /_matrix/client/r0/user_directory/search {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_user_directory_search_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_user_directory_search_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+	{% endif %}
+
+	{% if matrix_nginx_proxy_proxy_matrix_3pid_registration_enabled %}
+	location ~ ^/_matrix/client/r0/register/(email|msisdn)/requestToken$ {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_3pid_registration_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_3pid_registration_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+	}
+	{% endif %}
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	{#
+		This handles the Matrix Client API only.
+		The Matrix Federation API is handled by a separate vhost.
+	#}
+	location ~* ^({{ matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_prefix_regexes|join('|') }}) {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_client_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_client_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+
+		client_body_buffer_size 25M;
+		client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_client_api_client_max_body_size_mb }}M;
+		proxy_max_temp_file_size 0;
+	}
+
+	{#
+		We only handle the root URI for this redirect or homepage serving.
+		Unhandled URIs (mostly by `matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_prefix_regexes` above) should result in a 404,
+		instead of causing a redirect.
+		See: https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1058
+	#}
+	location ~* ^/$ {
+		{% if matrix_nginx_proxy_proxy_matrix_client_redirect_root_uri_to_domain %}
+			return 302 $scheme://{{ matrix_nginx_proxy_proxy_matrix_client_redirect_root_uri_to_domain }}$request_uri;
+		{% else %}
+			rewrite ^/$ /_matrix/static/ last;
+		{% endif %}
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+	server_name {{ matrix_nginx_proxy_proxy_matrix_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		{% if matrix_nginx_proxy_proxy_matrix_nginx_status_enabled %}
+			{{ render_nginx_status_location_block(matrix_nginx_proxy_proxy_matrix_nginx_status_allowed_addresses) }}
+		{% endif %}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_matrix_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_matrix_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};	
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
+
+{% if matrix_nginx_proxy_proxy_matrix_federation_api_enabled %}
+{#
+	This federation vhost is a little special.
+	It serves federation over HTTP or HTTPS, depending on `matrix_nginx_proxy_https_enabled`.
+#}
+server {
+	{% if matrix_nginx_proxy_https_enabled %}
+		listen {{ matrix_nginx_proxy_proxy_matrix_federation_port }} ssl http2;
+		listen [::]:{{ matrix_nginx_proxy_proxy_matrix_federation_port }} ssl http2;
+	{% else %}
+		listen {{ matrix_nginx_proxy_proxy_matrix_federation_port }};
+	{% endif %}
+
+	server_name {{ matrix_nginx_proxy_proxy_matrix_hostname }};
+	server_tokens off;
+
+	root /dev/null;
+
+	gzip on;
+	gzip_types text/plain application/json;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		ssl_certificate {{ matrix_nginx_proxy_proxy_matrix_federation_api_ssl_certificate }};
+		ssl_certificate_key {{ matrix_nginx_proxy_proxy_matrix_federation_api_ssl_certificate_key }};
+
+		ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+		{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+			ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+		{% endif %}
+		ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+		{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+			ssl_stapling on;
+			ssl_stapling_verify on;
+			ssl_trusted_certificate {{ matrix_nginx_proxy_proxy_matrix_federation_api_ssl_trusted_certificate }};
+		{% endif %}
+		
+		{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+			ssl_session_tickets off;
+		{% endif %}
+		ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+		ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+	{% endif %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_matrix_federation_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_matrix_federation_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+
+		client_body_buffer_size 25M;
+		client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_federation_api_client_max_body_size_mb }}M;
+		proxy_max_temp_file_size 0;
+	}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-grafana.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-grafana.conf.j2
new file mode 100644
index 000000000..0f7c43c57
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-grafana.conf.j2
@@ -0,0 +1,106 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	# duplicate X-Content-Type-Options & X-Frame-Options header
+	# Enabled by grafana by default
+	# add_header X-Content-Type-Options nosniff;
+	# add_header X-Frame-Options SAMEORIGIN;
+	add_header Referrer-Policy "strict-origin-when-cross-origin";
+
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+	proxy_cookie_path / "/; HTTPOnly; Secure";
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_grafana_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-grafana:3000";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:3000;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+
+	server_name {{ matrix_nginx_proxy_proxy_grafana_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_grafana_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_grafana_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_grafana_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != "" %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_grafana_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};	
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-jitsi.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-jitsi.conf.j2
new file mode 100644
index 000000000..0ccda7d31
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-jitsi.conf.j2
@@ -0,0 +1,140 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Content-Type-Options nosniff;
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+{% for configuration_block in matrix_nginx_proxy_proxy_jitsi_additional_server_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-jitsi-web:80";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:13080;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+	}
+
+	# colibri (JVB) websockets
+	location ~ ^/colibri-ws/([a-zA-Z0-9-\.]+)/(.*) {
+		{% if matrix_nginx_proxy_enabled %}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-jitsi-jvb:9090";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:13090;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection "upgrade";
+
+		proxy_http_version 1.1;
+
+		tcp_nodelay on;
+	}
+
+	# XMPP websocket
+	location = /xmpp-websocket {
+		{% if matrix_nginx_proxy_enabled %}
+			resolver 127.0.0.11 valid=5s;
+			set $backend {{ matrix_jitsi_xmpp_bosh_url_base }};
+			proxy_pass $backend/xmpp-websocket;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:5280;
+		{% endif %}
+		proxy_set_header Host $host;
+
+		proxy_http_version 1.1;
+		proxy_read_timeout 900s;
+		proxy_set_header Connection "upgrade";
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+		tcp_nodelay on;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+	server_name {{ matrix_nginx_proxy_proxy_jitsi_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_jitsi_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_jitsi_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_jitsi_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_jitsi_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-riot-web.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-riot-web.conf.j2
new file mode 100644
index 000000000..d153d5c20
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-riot-web.conf.j2
@@ -0,0 +1,87 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	{% if matrix_nginx_proxy_floc_optout_enabled %}
+		add_header Permissions-Policy interest-cohort=() always;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_riot_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	location / {
+		return 301 https://{{ matrix_nginx_proxy_proxy_element_hostname }}$request_uri;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+
+	server_name {{ matrix_nginx_proxy_proxy_riot_compat_redirect_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_riot_compat_redirect_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_riot_compat_redirect_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_riot_compat_redirect_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_riot_compat_redirect_hostname }}/chain.pem;
+	{% endif %}
+
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-sygnal.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-sygnal.conf.j2
new file mode 100644
index 000000000..d57604347
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-sygnal.conf.j2
@@ -0,0 +1,97 @@
+#jinja2: lstrip_blocks: "True"
+
+{% macro render_vhost_directives() %}
+	gzip on;
+	gzip_types text/plain application/json application/javascript text/css image/x-icon font/ttf image/gif;
+	{% if matrix_nginx_proxy_hsts_preload_enabled %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+	{% else %}
+		add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+	{% endif %}
+	add_header X-XSS-Protection "{{ matrix_nginx_proxy_xss_protection }}";
+	add_header X-Content-Type-Options nosniff;
+	add_header X-Frame-Options DENY;
+
+{% for configuration_block in matrix_nginx_proxy_proxy_sygnal_additional_server_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "matrix-sygnal:6000";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://127.0.0.1:6000;
+		{% endif %}
+
+		proxy_set_header Host $host;
+		proxy_set_header X-Forwarded-For $remote_addr;
+		proxy_set_header X-Forwarded-Proto $scheme;
+	}
+{% endmacro %}
+
+server {
+	listen {{ 8080 if matrix_nginx_proxy_enabled else 80 }};
+	server_name {{ matrix_nginx_proxy_proxy_sygnal_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	{% if matrix_nginx_proxy_https_enabled %}
+		location /.well-known/acme-challenge {
+			{% if matrix_nginx_proxy_enabled %}
+				{# Use the embedded DNS resolver in Docker containers to discover the service #}
+				resolver 127.0.0.11 valid=5s;
+				set $backend "matrix-certbot:8080";
+				proxy_pass http://$backend;
+			{% else %}
+				{# Generic configuration for use outside of our container setup #}
+				proxy_pass http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }};
+			{% endif %}
+		}
+
+		location / {
+			return 301 https://$http_host$request_uri;
+		}
+	{% else %}
+		{{ render_vhost_directives() }}
+	{% endif %}
+}
+
+{% if matrix_nginx_proxy_https_enabled %}
+server {
+	listen {{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+	listen [::]:{{ 8443 if matrix_nginx_proxy_enabled else 443 }} ssl http2;
+
+	server_name {{ matrix_nginx_proxy_proxy_sygnal_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	ssl_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_sygnal_hostname }}/fullchain.pem;
+	ssl_certificate_key {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_sygnal_hostname }}/privkey.pem;
+
+	ssl_protocols {{ matrix_nginx_proxy_ssl_protocols }};
+	{% if matrix_nginx_proxy_ssl_ciphers != '' %}
+	ssl_ciphers {{ matrix_nginx_proxy_ssl_ciphers }};
+	{% endif %}
+	ssl_prefer_server_ciphers {{ matrix_nginx_proxy_ssl_prefer_server_ciphers }};
+
+	{% if matrix_nginx_proxy_ocsp_stapling_enabled %}
+		ssl_stapling on;
+		ssl_stapling_verify on;
+		ssl_trusted_certificate {{ matrix_ssl_config_dir_path }}/live/{{ matrix_nginx_proxy_proxy_sygnal_hostname }}/chain.pem;
+	{% endif %}
+	
+	{% if matrix_nginx_proxy_ssl_session_tickets_off %}
+		ssl_session_tickets off;
+	{% endif %}
+	ssl_session_cache {{ matrix_nginx_proxy_ssl_session_cache }};
+	ssl_session_timeout {{ matrix_nginx_proxy_ssl_session_timeout }};
+
+	{{ render_vhost_directives() }}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-synapse.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-synapse.conf.j2
new file mode 100644
index 000000000..db111090c
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/matrix-synapse.conf.j2
@@ -0,0 +1,231 @@
+#jinja2: lstrip_blocks: "True"
+
+{% set generic_workers = matrix_nginx_proxy_synapse_workers_list|selectattr('type', 'equalto', 'generic_worker')|list %}
+{% set media_repository_workers = matrix_nginx_proxy_synapse_workers_list|selectattr('type', 'equalto', 'media_repository')|list %}
+{% set user_dir_workers = matrix_nginx_proxy_synapse_workers_list|selectattr('type', 'equalto', 'user_dir')|list %}
+{% set frontend_proxy_workers = matrix_nginx_proxy_synapse_workers_list|selectattr('type', 'equalto', 'frontend_proxy')|list %}
+{% if matrix_nginx_proxy_synapse_workers_enabled %}
+	# Round Robin "upstream" pools for workers
+
+	{% if generic_workers %}
+	upstream generic_worker_upstream {
+		# ensures that requests from the same client will always be passed
+		# to the same server (except when this server is unavailable)
+		hash $http_x_forwarded_for;
+
+		{% for worker in generic_workers %}
+			{% if matrix_nginx_proxy_enabled %}
+				server "matrix-synapse-worker-{{ worker.type }}-{{ worker.instanceId }}:{{ worker.port }}";
+			{% else %}
+				server "127.0.0.1:{{ worker.port }}";
+			{% endif %}
+		{% endfor %}
+	}
+	{% endif %}
+
+	{% if frontend_proxy_workers %}
+	upstream frontend_proxy_upstream {
+		{% for worker in frontend_proxy_workers %}
+			{% if matrix_nginx_proxy_enabled %}
+				server "matrix-synapse-worker-{{ worker.type }}-{{ worker.instanceId }}:{{ worker.port }}";
+			{% else %}
+				server "127.0.0.1:{{ worker.port }}";
+			{% endif %}
+		{% endfor %}
+	}
+	{% endif %}
+
+	{% if media_repository_workers %}
+	upstream media_repository_upstream {
+		{% for worker in media_repository_workers %}
+			{% if matrix_nginx_proxy_enabled %}
+				server "matrix-synapse-worker-{{ worker.type }}-{{ worker.instanceId }}:{{ worker.port }}";
+			{% else %}
+				server "127.0.0.1:{{ worker.port }}";
+			{% endif %}
+		{% endfor %}
+	}
+	{% endif %}
+
+	{% if user_dir_workers %}
+	upstream user_dir_upstream {
+		{% for worker in user_dir_workers %}
+			{% if matrix_nginx_proxy_enabled %}
+				server "matrix-synapse-worker-{{ worker.type }}-{{ worker.instanceId }}:{{ worker.port }}";
+			{% else %}
+				server "127.0.0.1:{{ worker.port }}";
+			{% endif %}
+		{% endfor %}
+	}
+	{% endif %}
+{% endif %}
+
+server {
+	listen 12080;
+	server_name {{ matrix_nginx_proxy_proxy_synapse_hostname }};
+
+	server_tokens off;
+	root /dev/null;
+
+	gzip on;
+	gzip_types text/plain application/json;
+
+	{% if matrix_nginx_proxy_synapse_workers_enabled %}
+		{# Workers redirects BEGIN #}
+
+		{% if generic_workers %}
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappgeneric_worker
+			{% for location in matrix_nginx_proxy_synapse_generic_worker_client_server_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://generic_worker_upstream$request_uri;
+				proxy_set_header Host $host;
+			}
+			{% endfor %}
+		{% endif %}
+
+		{% if media_repository_workers %}
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappmedia_repository
+			{% for location in matrix_nginx_proxy_synapse_media_repository_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://media_repository_upstream$request_uri;
+				proxy_set_header Host $host;
+
+				client_body_buffer_size 25M;
+				client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_client_api_client_max_body_size_mb }}M;
+				proxy_max_temp_file_size 0;
+			}
+			{% endfor %}
+		{% endif %}
+
+		{% if user_dir_workers %}
+			# FIXME: obsolete if matrix_nginx_proxy_proxy_matrix_user_directory_search_enabled is set
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappuser_dir
+			{% for location in matrix_nginx_proxy_synapse_user_dir_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://user_dir_upstream$request_uri;
+				proxy_set_header Host $host;
+			}
+			{% endfor %}
+		{% endif %}
+
+		{% if frontend_proxy_workers %}
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappfrontend_proxy
+			{% for location in matrix_nginx_proxy_synapse_frontend_proxy_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://frontend_proxy_upstream$request_uri;
+				proxy_set_header Host $host;
+			}
+			{% endfor %}
+			{% if matrix_nginx_proxy_synapse_presence_disabled %}
+			# FIXME: keep in sync with synapse workers documentation manually
+			location ~ ^/_matrix/client/(api/v1|r0|unstable)/presence/[^/]+/status {
+				proxy_pass http://frontend_proxy_upstream$request_uri;
+				proxy_set_header Host $host;
+			}
+			{% endif %}
+		{% endif %}
+		{# Workers redirects END #}
+	{% endif %}
+
+
+	{% for configuration_block in matrix_nginx_proxy_proxy_synapse_additional_server_configuration_blocks %}
+		{{- configuration_block }}
+	{% endfor %}
+
+	{% if matrix_nginx_proxy_proxy_synapse_metrics %}
+	location /_synapse/metrics {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_synapse_metrics_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_synapse_metrics_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+
+		{% if matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_enabled %}
+			auth_basic "protected";
+			auth_basic_user_file /nginx-data/matrix-synapse-metrics-htpasswd;
+		{% endif %}
+	}
+	{% endif %}
+
+	{# Everything else just goes to the API server ##}
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_synapse_client_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_synapse_client_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+
+		client_body_buffer_size 25M;
+		client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_client_api_client_max_body_size_mb }}M;
+		proxy_max_temp_file_size 0;
+	}
+}
+
+{% if matrix_nginx_proxy_proxy_synapse_federation_api_enabled %}
+server {
+	listen 12088;
+
+	server_name {{ matrix_nginx_proxy_proxy_synapse_hostname }};
+	server_tokens off;
+
+	root /dev/null;
+
+	gzip on;
+	gzip_types text/plain application/json;
+
+	{% if matrix_nginx_proxy_synapse_workers_enabled %}
+		{% if generic_workers %}
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappgeneric_worker
+			{% for location in matrix_nginx_proxy_synapse_generic_worker_federation_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://generic_worker_upstream$request_uri;
+				proxy_set_header Host $host;
+			}
+			{% endfor %}
+		{% endif %}
+		{% if media_repository_workers %}
+			# https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappmedia_repository
+			{% for location in matrix_nginx_proxy_synapse_media_repository_locations %}
+			location ~ {{ location }} {
+				proxy_pass http://media_repository_upstream$request_uri;
+				proxy_set_header Host $host;
+
+				client_body_buffer_size 25M;
+				client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_federation_api_client_max_body_size_mb }}M;
+				proxy_max_temp_file_size 0;
+			}
+			{% endfor %}
+		{% endif %}
+	{% endif %}
+
+	location / {
+		{% if matrix_nginx_proxy_enabled %}
+			{# Use the embedded DNS resolver in Docker containers to discover the service #}
+			resolver 127.0.0.11 valid=5s;
+			set $backend "{{ matrix_nginx_proxy_proxy_synapse_federation_api_addr_with_container }}";
+			proxy_pass http://$backend;
+		{% else %}
+			{# Generic configuration for use outside of our container setup #}
+			proxy_pass http://{{ matrix_nginx_proxy_proxy_synapse_federation_api_addr_sans_container }};
+		{% endif %}
+
+		proxy_set_header Host $host;
+
+		client_body_buffer_size 25M;
+		client_max_body_size {{ matrix_nginx_proxy_proxy_matrix_federation_api_client_max_body_size_mb }}M;
+		proxy_max_temp_file_size 0;
+	}
+}
+{% endif %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/conf.d/nginx-http.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/conf.d/nginx-http.conf.j2
new file mode 100644
index 000000000..beea6afa1
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/conf.d/nginx-http.conf.j2
@@ -0,0 +1,14 @@
+#jinja2: lstrip_blocks: "True"
+# The default is aligned to the CPU's cache size,
+# which can sometimes be too low to handle our 2 vhosts (Synapse and Element).
+#
+# Thus, we ensure a larger bucket size value is used.
+server_names_hash_bucket_size 64;
+
+{% if matrix_nginx_proxy_http_level_resolver %}
+	resolver {{ matrix_nginx_proxy_http_level_resolver }};
+{% endif %}
+
+{% for configuration_block in matrix_nginx_proxy_proxy_http_additional_server_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/matrix-synapse-metrics-htpasswd.j2 b/roles/matrix-nginx-proxy/templates/nginx/matrix-synapse-metrics-htpasswd.j2
new file mode 100644
index 000000000..1a7247ace
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/matrix-synapse-metrics-htpasswd.j2
@@ -0,0 +1,3 @@
+#jinja2: lstrip_blocks: "True"
+# User and password for protecting /_synapse/metrics URI
+prometheus:{{ matrix_nginx_proxy_proxy_synapse_metrics_basic_auth_key }}
diff --git a/roles/matrix-nginx-proxy/templates/nginx/nginx.conf.j2 b/roles/matrix-nginx-proxy/templates/nginx/nginx.conf.j2
new file mode 100644
index 000000000..9ec7fa560
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/nginx/nginx.conf.j2
@@ -0,0 +1,61 @@
+#jinja2: lstrip_blocks: "True"
+# This is a custom nginx configuration file that we use in the container (instead of the default one),
+# because it allows us to run nginx with a non-root user.
+#
+# For this to work, the default vhost file (`/etc/nginx/conf.d/default.conf`) also needs to be removed.
+#
+# The following changes have been done compared to a default nginx configuration file:
+# - various temp paths are changed to `/tmp`, so that a non-root user can write to them
+# - the `user` directive was removed, as we don't want nginx to switch users
+
+worker_processes {{ matrix_nginx_proxy_worker_processes }};
+error_log /var/log/nginx/error.log warn;
+pid /tmp/nginx.pid;
+{% for configuration_block in matrix_nginx_proxy_proxy_additional_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+
+events {
+	worker_connections {{ matrix_nginx_proxy_worker_connections }};
+{% for configuration_block in matrix_nginx_proxy_proxy_event_additional_configuration_blocks %}
+	{{- configuration_block }}
+{% endfor %}
+}
+
+
+http {
+	proxy_temp_path /tmp/proxy_temp;
+	client_body_temp_path /tmp/client_temp;
+	fastcgi_temp_path /tmp/fastcgi_temp;
+	uwsgi_temp_path /tmp/uwsgi_temp;
+	scgi_temp_path /tmp/scgi_temp;
+
+	include /etc/nginx/mime.types;
+	default_type application/octet-stream;
+
+	log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+					'$status $body_bytes_sent "$http_referer" '
+					'"$http_user_agent" "$http_x_forwarded_for"';
+
+	{% if matrix_nginx_proxy_access_log_enabled %}
+	access_log /var/log/nginx/access.log main;
+	{% else %}
+	access_log off;
+	{% endif %}
+
+	sendfile on;
+	#tcp_nopush on;
+
+	keepalive_timeout 65;
+
+	server_tokens off;
+
+	#gzip on;
+	{# Map directive needed for proxied WebSocket upgrades #}
+	map $http_upgrade $connection_upgrade {
+		default upgrade;
+		''      close;
+	}
+
+	include /etc/nginx/conf.d/*.conf;
+}
diff --git a/roles/matrix-nginx-proxy/templates/systemd/matrix-nginx-proxy.service.j2 b/roles/matrix-nginx-proxy/templates/systemd/matrix-nginx-proxy.service.j2
new file mode 100755
index 000000000..c4000fa9b
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/systemd/matrix-nginx-proxy.service.j2
@@ -0,0 +1,58 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix nginx-proxy server
+{% for service in matrix_nginx_proxy_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_nginx_proxy_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-nginx-proxy 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-nginx-proxy 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-nginx-proxy \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,noexec,nosuid,size={{ matrix_nginx_proxy_tmp_directory_size_mb }}m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_nginx_proxy_container_http_host_bind_port %}
+			-p {{ matrix_nginx_proxy_container_http_host_bind_port }}:8080 \
+			{% endif %}
+			{% if matrix_nginx_proxy_https_enabled and matrix_nginx_proxy_container_https_host_bind_port %}
+			-p {{ matrix_nginx_proxy_container_https_host_bind_port }}:8443 \
+			{% endif %}
+			{% if matrix_nginx_proxy_proxy_matrix_federation_api_enabled and matrix_nginx_proxy_container_federation_host_bind_port %}
+			-p {{ matrix_nginx_proxy_container_federation_host_bind_port }}:{{ matrix_nginx_proxy_proxy_matrix_federation_port }} \
+			{% endif %}
+			--mount type=bind,src={{ matrix_nginx_proxy_base_path }}/nginx.conf,dst=/etc/nginx/nginx.conf,ro \
+			--mount type=bind,src={{ matrix_nginx_proxy_data_path }},dst={{ matrix_nginx_proxy_data_path_in_container }},ro \
+			--mount type=bind,src={{ matrix_nginx_proxy_confd_path }},dst=/etc/nginx/conf.d,ro \
+			{% if matrix_ssl_retrieval_method != 'none' %}
+			--mount type=bind,src={{ matrix_ssl_config_dir_path }},dst={{ matrix_ssl_config_dir_path }},ro \
+			{% endif %}
+			--mount type=bind,src={{ matrix_static_files_base_path }},dst={{ matrix_static_files_base_path }},ro \
+			{% for volume in matrix_nginx_proxy_container_additional_volumes %}
+			-v {{ volume.src }}:{{ volume.dst }}:{{ volume.options }} \
+			{% endfor %}
+			{% for arg in matrix_nginx_proxy_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_nginx_proxy_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-nginx-proxy 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-nginx-proxy 2>/dev/null'
+ExecReload={{ matrix_host_command_docker }} exec matrix-nginx-proxy /usr/sbin/nginx -s reload
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-nginx-proxy
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.service.j2 b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.service.j2
new file mode 100644
index 000000000..c14905ce5
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.service.j2
@@ -0,0 +1,7 @@
+[Unit]
+Description=Renews Let's Encrypt SSL certificates
+
+[Service]
+Type=oneshot
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStart={{ matrix_local_bin_path }}/matrix-ssl-lets-encrypt-certificates-renew
diff --git a/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.timer.j2 b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.timer.j2
new file mode 100644
index 000000000..b1e1c21e8
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-lets-encrypt-certificates-renew.timer.j2
@@ -0,0 +1,10 @@
+[Unit]
+Description=Renews Let's Encrypt SSL certificates periodically
+
+[Timer]
+Unit=matrix-ssl-lets-encrypt-certificates-renew.service
+OnCalendar=*-*-* 04:00:00
+RandomizedDelaySec=2h
+
+[Install]
+WantedBy=timers.target
diff --git a/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.service.j2 b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.service.j2
new file mode 100644
index 000000000..851655baa
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.service.j2
@@ -0,0 +1,6 @@
+[Unit]
+Description=Reloads matrix-nginx-proxy so that new SSL certificates can kick in
+
+[Service]
+Type=oneshot
+ExecStart={{ matrix_host_command_systemctl }} reload matrix-nginx-proxy.service
diff --git a/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.timer.j2 b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.timer.j2
new file mode 100644
index 000000000..09cb6dad7
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/systemd/matrix-ssl-nginx-proxy-reload.timer.j2
@@ -0,0 +1,10 @@
+[Unit]
+Description=Reloads matrix-nginx-proxy periodically so that new SSL certificates can kick in
+
+[Timer]
+Unit=matrix-ssl-nginx-proxy-reload.service
+OnCalendar=*-*-* 06:30:00
+RandomizedDelaySec=1h
+
+[Install]
+WantedBy=timers.target
diff --git a/roles/matrix-nginx-proxy/templates/usr-local-bin/matrix-ssl-lets-encrypt-certificates-renew.j2 b/roles/matrix-nginx-proxy/templates/usr-local-bin/matrix-ssl-lets-encrypt-certificates-renew.j2
new file mode 100644
index 000000000..bc45e85e9
--- /dev/null
+++ b/roles/matrix-nginx-proxy/templates/usr-local-bin/matrix-ssl-lets-encrypt-certificates-renew.j2
@@ -0,0 +1,31 @@
+#jinja2: lstrip_blocks: "True"
+#!/bin/bash
+
+# For renewal to work, matrix-nginx-proxy (or another webserver, if matrix-nginx-proxy is disabled)
+# need to forward requests for `/.well-known/acme-challenge` to the certbot container.
+#
+# This can happen inside the container network by proxying to `http://matrix-certbot:8080`
+# or outside (on the host) by proxying to `http://127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }}`.
+
+docker run \
+	--rm \
+	--name=matrix-certbot \
+	--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+	--cap-drop=ALL \
+	--network="{{ matrix_docker_network }}" \
+	-p 127.0.0.1:{{ matrix_ssl_lets_encrypt_certbot_standalone_http_port }}:8080 \
+	--mount type=bind,src={{ matrix_ssl_config_dir_path }},dst=/etc/letsencrypt \
+	--mount type=bind,src={{ matrix_ssl_log_dir_path }},dst=/var/log/letsencrypt \
+	{{ matrix_ssl_lets_encrypt_certbot_docker_image }} \
+	renew \
+		--non-interactive \
+		--work-dir=/tmp \
+		--http-01-port 8080 \
+		{% if matrix_ssl_lets_encrypt_staging %}
+			--staging \
+		{% endif %}
+		--standalone \
+		--preferred-challenges http \
+		--agree-tos \
+		--email={{ matrix_ssl_lets_encrypt_support_email }} \
+		--no-random-sleep-on-renew
diff --git a/roles/matrix-nginx-proxy/vars/main.yml b/roles/matrix-nginx-proxy/vars/main.yml
new file mode 100644
index 000000000..5c51fe5bd
--- /dev/null
+++ b/roles/matrix-nginx-proxy/vars/main.yml
@@ -0,0 +1,18 @@
+---
+
+# Tells whether this role had executed or not. Toggled to `true` during runtime.
+matrix_nginx_proxy_role_executed: false
+
+matrix_ssl_renewal_systemd_units_list:
+  - name: matrix-ssl-lets-encrypt-certificates-renew.service
+    applicable: "{{ matrix_ssl_retrieval_method == 'lets-encrypt' }}"
+    enableable: false
+  - name: matrix-ssl-lets-encrypt-certificates-renew.timer
+    applicable: "{{ matrix_ssl_retrieval_method == 'lets-encrypt' }}"
+    enableable: true
+  - name: matrix-ssl-nginx-proxy-reload.service
+    applicable: "{{ matrix_ssl_retrieval_method == 'lets-encrypt' and matrix_nginx_proxy_enabled|bool }}"
+    enableable: false
+  - name: matrix-ssl-nginx-proxy-reload.timer
+    applicable: "{{ matrix_ssl_retrieval_method == 'lets-encrypt' and matrix_nginx_proxy_enabled|bool }}"
+    enableable: true
diff --git a/roles/matrix-postgres/defaults/main.yml b/roles/matrix-postgres/defaults/main.yml
new file mode 100644
index 000000000..9c1cac9a9
--- /dev/null
+++ b/roles/matrix-postgres/defaults/main.yml
@@ -0,0 +1,95 @@
+matrix_postgres_enabled: true
+
+matrix_postgres_connection_hostname: "matrix-postgres"
+matrix_postgres_connection_port: 5432
+matrix_postgres_connection_username: "matrix"
+matrix_postgres_connection_password: ""
+matrix_postgres_db_name: "matrix"
+
+matrix_postgres_base_path: "{{ matrix_base_data_path }}/postgres"
+matrix_postgres_data_path: "{{ matrix_postgres_base_path }}/data"
+
+matrix_postgres_architecture: amd64
+
+# matrix_postgres_docker_image_suffix controls whether we use Alpine-based images (`-alpine`) or the normal Debian-based images.
+# Alpine-based Postgres images are smaller and we usually prefer them, but they don't work on ARM32 (tested on a Raspberry Pi 3 running Raspbian 10.7).
+# On ARM32, `-alpine` images fail with the following error:
+# > LOG:  startup process (PID 37) was terminated by signal 11: Segmentation fault
+matrix_postgres_docker_image_suffix: "{{ '-alpine' if matrix_postgres_architecture in ['amd64', 'arm64'] else '' }}"
+
+matrix_postgres_docker_image_v9: "{{ matrix_container_global_registry_prefix }}postgres:9.6.22{{ matrix_postgres_docker_image_suffix }}"
+matrix_postgres_docker_image_v10: "{{ matrix_container_global_registry_prefix }}postgres:10.17{{ matrix_postgres_docker_image_suffix }}"
+matrix_postgres_docker_image_v11: "{{ matrix_container_global_registry_prefix }}postgres:11.12{{ matrix_postgres_docker_image_suffix }}"
+matrix_postgres_docker_image_v12: "{{ matrix_container_global_registry_prefix }}postgres:12.7{{ matrix_postgres_docker_image_suffix }}"
+matrix_postgres_docker_image_v13: "{{ matrix_container_global_registry_prefix }}postgres:13.3{{ matrix_postgres_docker_image_suffix }}"
+matrix_postgres_docker_image_latest: "{{ matrix_postgres_docker_image_v13 }}"
+
+# This variable is assigned at runtime. Overriding its value has no effect.
+matrix_postgres_docker_image_to_use: '{{ matrix_postgres_docker_image_latest }}'
+
+matrix_postgres_docker_image_force_pull: "{{ matrix_postgres_docker_image_to_use.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_postgres_container_extra_arguments: []
+
+# A list of extra arguments to pass to the postgres process
+# e.g. "-c 'max_connections=200'"
+matrix_postgres_process_extra_arguments: []
+
+# Controls whether the matrix-postgres container exposes a port (tcp/5432 in the
+# container) that can be used to access the database from outside the container (e.g. with psql)
+#
+# psql postgresql://username:password@localhost:<port>/database_name
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:5432"), or empty string to not expose.
+matrix_postgres_container_postgres_bind_port: ""
+
+# A list of additional (databases and their credentials) to create.
+#
+# Example:
+# matrix_postgres_additional_databases:
+#   - name: matrix_appservice_discord
+#     username: matrix_appservice_discord
+#     password: some_password
+#   - name: matrix_appservice_slack
+#     username: matrix_appservice_slack
+#     password: some_password
+matrix_postgres_additional_databases: []
+
+# A list of roles/users to avoid creating when importing (or upgrading) the database.
+# If a dump file contains the roles and they've also been created beforehand (see `matrix_postgres_additional_databases`),
+# importing would fail.
+# We either need to not create them or to ignore the `CREATE ROLE` statements in the dump.
+matrix_postgres_import_roles_to_ignore: [matrix_postgres_connection_username]
+
+matrix_postgres_import_roles_ignore_regex: "^CREATE ROLE ({{ matrix_postgres_import_roles_to_ignore|join('|') }});"
+
+# A list of databases to avoid creating when importing (or upgrading) the database.
+# If a dump file contains the databases and they've also been created beforehand (see `matrix_postgres_additional_databases`),
+# importing would fail.
+# We either need to not create them or to ignore the `CREATE DATABASE` statements in the dump.
+matrix_postgres_import_databases_to_ignore: [matrix_postgres_db_name]
+
+matrix_postgres_import_databases_ignore_regex: "^CREATE DATABASE ({{ matrix_postgres_import_databases_to_ignore|join('|') }})\\s"
+
+# The number of seconds to wait after starting `matrix-postgres.service`
+# and before trying to run queries for creating additional databases/users against it.
+#
+# For most (subsequent) runs, Postgres would already be running, so no waiting will be happening at all.
+#
+# On ARM, we wait some more. ARM32 devices are especially known for being slow.
+# ARM64 likely don't need such a long delay, but it doesn't hurt too much having it.
+matrix_postgres_additional_databases_postgres_start_wait_timeout_seconds: "{{ 45 if matrix_postgres_architecture in ['arm32', 'arm64'] else 15 }}"
+
+
+matrix_postgres_pgloader_container_image_self_build: false
+matrix_postgres_pgloader_container_image_self_build_repo: "https://github.com/illagrenan/pgloader-docker.git"
+matrix_postgres_pgloader_container_image_self_build_repo_branch: "v{{ matrix_postgres_pgloader_docker_image_tag }}"
+matrix_postgres_pgloader_container_image_self_build_src_path: "{{ matrix_postgres_base_path }}/pgloader-container-src"
+
+# We use illagrenan/pgloader, instead of the more official dimitri/pgloader image,
+# because the official one only provides a `latest` tag.
+matrix_postgres_pgloader_docker_image: "{{ matrix_postgres_pgloader_docker_image_name_prefix }}illagrenan/pgloader:{{ matrix_postgres_pgloader_docker_image_tag }}"
+matrix_postgres_pgloader_docker_image_name_prefix: "{{ 'localhost/' if matrix_postgres_pgloader_container_image_self_build else matrix_container_global_registry_prefix }}"
+matrix_postgres_pgloader_docker_image_tag: "3.6.2"
+matrix_postgres_pgloader_docker_image_force_pull: "{{ matrix_postgres_pgloader_docker_image.endswith(':latest') }}"
diff --git a/roles/matrix-postgres/tasks/import_generic_sqlite_db.yml b/roles/matrix-postgres/tasks/import_generic_sqlite_db.yml
new file mode 100644
index 000000000..a42c6f552
--- /dev/null
+++ b/roles/matrix-postgres/tasks/import_generic_sqlite_db.yml
@@ -0,0 +1,97 @@
+---
+
+# Pre-checks
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot import."
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `sqlite_database_path` variable needs to be provided to this playbook, via --extra-vars"
+  when: "sqlite_database_path is not defined or sqlite_database_path.startswith('<')"
+
+- name: Check if the provided SQLite database file exists
+  stat:
+    path: "{{ sqlite_database_path }}"
+  register: sqlite_database_path_stat_result
+
+- name: Fail if provided SQLite database file doesn't exist
+  fail:
+    msg: "File cannot be found on the server at {{ sqlite_database_path }}"
+  when: "not sqlite_database_path_stat_result.stat.exists"
+
+# We either expect `postgres_db_connection_string` specifying a full Postgres database connection string,
+# or `postgres_connection_string_variable_name`, specifying a name of a variable, which contains a valid connection string.
+
+- block:
+    - name: Fail if postgres_connection_string_variable_name points to an undefined variable
+      fail: msg="postgres_connection_string_variable_name is defined, but there is no variable with the name `{{ postgres_connection_string_variable_name }}`"
+      when: "postgres_connection_string_variable_name not in vars"
+
+    - name: Get Postgres connection string from variable
+      set_fact:
+        postgres_db_connection_string: "{{ lookup('vars', postgres_connection_string_variable_name) }}"
+  when: 'postgres_connection_string_variable_name is defined'
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: >-
+      Either a `postgres_db_connection_string` variable or a `postgres_connection_string_variable_name` needs to be provided to this playbook, via `--extra-vars`.
+      Example: `--extra-vars="postgres_db_connection_string=postgresql://username:password@localhost:<port>/database_name"` or `--extra-vars="postgres_connection_string_variable_name=matrix_appservice_discord_database_connString"`
+  when: "postgres_db_connection_string is not defined or not postgres_db_connection_string.startswith('postgresql://')"
+
+
+# Defaults
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+
+# Actual import work
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+  register: matrix_postgres_service_start_result
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+  when: "matrix_postgres_service_start_result.changed|bool"
+
+- name: Import SQLite database from {{ sqlite_database_path }} into Postgres
+  command:
+    cmd: >-
+      {{ matrix_host_command_docker }} run
+      --rm
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --mount type=bind,src={{ sqlite_database_path }},dst=/in.db,ro
+      --entrypoint=/bin/sh
+      {{ matrix_postgres_pgloader_docker_image }}
+      -c
+      'pgloader /in.db {{ postgres_db_connection_string }}'
+
+- name: Archive SQLite database ({{ sqlite_database_path }} -> {{ sqlite_database_path }}.backup)
+  command:
+    cmd: "mv {{ sqlite_database_path }} {{ sqlite_database_path }}.backup"
+
+- name: Inject result
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [
+          "NOTE: Your SQLite database file has been imported into Postgres. The original file has been moved from `{{ sqlite_database_path }}` to `{{ sqlite_database_path }}.backup`. When you've confirmed that the import went well and everything works, you should be able to safely delete this file."
+        ]
+      }}
diff --git a/roles/matrix-postgres/tasks/import_postgres.yml b/roles/matrix-postgres/tasks/import_postgres.yml
new file mode 100644
index 000000000..b8e932199
--- /dev/null
+++ b/roles/matrix-postgres/tasks/import_postgres.yml
@@ -0,0 +1,106 @@
+---
+
+# Pre-checks
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot import."
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `server_path_postgres_dump` variable needs to be provided to this playbook, via --extra-vars"
+  when: "server_path_postgres_dump is not defined or server_path_postgres_dump.startswith('<')"
+
+- name: Check if the provided Postgres dump file exists
+  stat:
+    path: "{{ server_path_postgres_dump }}"
+  register: result_server_path_postgres_dump_stat
+
+- name: Fail if provided Postgres dump file doesn't exists
+  fail:
+    msg: "File cannot be found on the server at {{ server_path_postgres_dump }}"
+  when: "not result_server_path_postgres_dump_stat.stat.exists"
+
+
+# Defaults
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+- name: Set postgres_import_wait_time, if not provided
+  set_fact:
+    postgres_import_wait_time: "{{ 7 * 86400 }}"
+  when: "postgres_import_wait_time|default('') == ''"
+
+# By default, we connect and import into the main (`matrix`) database.
+# Single-database dumps for Synapse may wish to import into `synapse` instead.
+- name: Set postgres_default_import_database, if not provided
+  set_fact:
+    postgres_default_import_database: "{{ matrix_postgres_db_name }}"
+  when: "postgres_default_import_database|default('') == ''"
+
+# Actual import work
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+
+- import_tasks: tasks/util/detect_existing_postgres_version.yml
+
+- name: Abort, if no existing Postgres version detected
+  fail:
+    msg: "Could not find existing Postgres installation"
+  when: "not matrix_postgres_detected_existing|bool"
+
+# Starting the database container had automatically created the default
+# role (`matrix_postgres_connection_username`) and database (`matrix_postgres_db_name`).
+# The dump most likely contains those same entries and would try to re-create them, leading to errors.
+# We need to skip over those lines.
+- name: Generate Postgres database import command
+  set_fact:
+    matrix_postgres_import_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-postgres-import
+      --log-driver=none
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      --mount type=bind,src={{ server_path_postgres_dump }},dst=/{{ server_path_postgres_dump|basename }},ro
+      --entrypoint=/bin/sh
+      {{ matrix_postgres_docker_image_latest }}
+      -c "cat /{{ server_path_postgres_dump|basename }} |
+      {{ 'gunzip |' if server_path_postgres_dump.endswith('.gz') else '' }}
+      grep -vE '{{ matrix_postgres_import_roles_ignore_regex }}' |
+      grep -vE '{{ matrix_postgres_import_databases_ignore_regex }}' |
+      psql -v ON_ERROR_STOP=1 -h matrix-postgres --dbname={{ postgres_default_import_database }}"
+
+# This is a hack.
+# See: https://ansibledaily.com/print-to-standard-output-without-escaping/
+#
+# We want to run `debug: msg=".."`, but that dumps it as JSON and escapes double quotes within it,
+# which ruins the command (`matrix_postgres_import_command`)
+- name: Note about Postgres importing alternative
+  set_fact:
+    dummy: true
+  with_items:
+    - >-
+        Importing Postgres database using the following command: `{{ matrix_postgres_import_command }}`.
+        If this crashes, you can stop Postgres (`systemctl stop matrix-postgres`),
+        delete its existing data (`rm -rf {{ matrix_postgres_data_path }}/*`), start it again (`systemctl start matrix-postgres`)
+        and manually run the above import command directly on the server.
+
+- name: Perform Postgres database import
+  command: "{{ matrix_postgres_import_command }}"
+  async: "{{ postgres_import_wait_time }}"
+  poll: 10
diff --git a/roles/matrix-postgres/tasks/import_synapse_sqlite_db.yml b/roles/matrix-postgres/tasks/import_synapse_sqlite_db.yml
new file mode 100644
index 000000000..ea15c5a86
--- /dev/null
+++ b/roles/matrix-postgres/tasks/import_synapse_sqlite_db.yml
@@ -0,0 +1,86 @@
+---
+
+# Pre-checks
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot import."
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `server_path_homeserver_db` variable needs to be provided to this playbook, via --extra-vars"
+  when: "server_path_homeserver_db is not defined or server_path_homeserver_db.startswith('<')"
+
+- name: Check if the provided SQLite homeserver.db file exists
+  stat:
+    path: "{{ server_path_homeserver_db }}"
+  register: result_server_path_homeserver_db_stat
+
+- name: Fail if provided SQLite homeserver.db file doesn't exist
+  fail:
+    msg: "File cannot be found on the server at {{ server_path_homeserver_db }}"
+  when: "not result_server_path_homeserver_db_stat.stat.exists"
+
+
+# Defaults
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+
+# Actual import work
+
+- name: Ensure matrix-postgres is stopped
+  service:
+    name: matrix-postgres
+    state: stopped
+    daemon_reload: yes
+
+- name: Ensure postgres data is wiped out
+  file:
+    path: "{{ matrix_postgres_data_path }}"
+    state: absent
+
+- name: Ensure postgres data path exists
+  file:
+    path: "{{ matrix_postgres_data_path }}"
+    state: directory
+    mode: 0700
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: restarted
+    daemon_reload: yes
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+
+# We don't use the `docker_container` module, because using it with `cap_drop` requires
+# a very recent version, which is not available for a lot of people yet.
+#
+# Also, some old `docker_container` versions were buggy and would leave containers behind
+# on failure, which we had to work around to allow retries (by re-running the playbook).
+- name: Import SQLite database into Postgres
+  command: |
+    docker run
+    --rm
+    --name=matrix-synapse-migrate
+    --log-driver=none
+    --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+    --cap-drop=ALL
+    --network={{ matrix_docker_network }}
+    --entrypoint=python
+    --mount type=bind,src={{ matrix_synapse_config_dir_path }},dst=/data
+    --mount type=bind,src={{ matrix_synapse_config_dir_path }},dst=/matrix-media-store-parent/media-store
+    --mount type=bind,src={{ server_path_homeserver_db }},dst=/{{ server_path_homeserver_db|basename }}
+    {{ matrix_synapse_docker_image }}
+    /usr/local/bin/synapse_port_db --sqlite-database /{{ server_path_homeserver_db|basename }} --postgres-config /data/homeserver.yaml
diff --git a/roles/matrix-postgres/tasks/init.yml b/roles/matrix-postgres/tasks/init.yml
new file mode 100644
index 000000000..a0f2ae60f
--- /dev/null
+++ b/roles/matrix-postgres/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-postgres.service'] }}"
+  when: matrix_postgres_enabled|bool
diff --git a/roles/matrix-postgres/tasks/main.yml b/roles/matrix-postgres/tasks/main.yml
new file mode 100644
index 000000000..b9c2ae7c9
--- /dev/null
+++ b/roles/matrix-postgres/tasks/main.yml
@@ -0,0 +1,43 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_postgres_enabled|bool"
+  tags:
+    - setup-all
+    - setup-postgres
+
+- import_tasks: "{{ role_path }}/tasks/setup_postgres.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-postgres
+
+- import_tasks: "{{ role_path }}/tasks/import_postgres.yml"
+  when: run_postgres_import|bool
+  tags:
+    - import-postgres
+
+# The `run_postgres_import_sqlite_db` variable had better be renamed to be consistent,
+# but that's a breaking change which may cause trouble for people.
+- import_tasks: "{{ role_path }}/tasks/import_synapse_sqlite_db.yml"
+  when: run_postgres_import_sqlite_db|bool
+  tags:
+    - import-synapse-sqlite-db
+
+# Perhaps we need a new variable here, instead of `run_postgres_import_sqlite_db`.
+- import_tasks: "{{ role_path }}/tasks/import_generic_sqlite_db.yml"
+  when: run_postgres_import_sqlite_db|bool
+  tags:
+    - import-generic-sqlite-db
+
+- import_tasks: "{{ role_path }}/tasks/upgrade_postgres.yml"
+  when: run_postgres_upgrade|bool
+  tags:
+    - upgrade-postgres
+
+- import_tasks: "{{ role_path }}/tasks/run_vacuum.yml"
+  when: run_postgres_vacuum|bool
+  tags:
+    - run-postgres-vacuum
diff --git a/roles/matrix-postgres/tasks/migrate_postgres_data_directory.yml b/roles/matrix-postgres/tasks/migrate_postgres_data_directory.yml
new file mode 100644
index 000000000..ef5fbf47d
--- /dev/null
+++ b/roles/matrix-postgres/tasks/migrate_postgres_data_directory.yml
@@ -0,0 +1,72 @@
+---
+
+# We used to store Postgres data directly under `/matrix/postgres` (what is now considered `matrix_postgres_base_path`).
+#
+# From now on, we expect to store Postgres data one directory below now (`/matrix/postgres/data` - `matrix_postgres_data_path`).
+# We wish to use the base directory for other purposes (storing environment variable files, etc.).
+# Mixing those with the Postgres data is no good and it leads to Postgres's `initdb` complaining to initialize
+# a database in a non-empty directory.
+#
+# For this reason, we store the Postgres data in `/matrix/postgres/data` and need to relocate any installations
+# which still store it in the parent directory (`/matrix/postgres`).
+
+- name: Check if old Postgres data directory is used
+  stat:
+    path: "{{ matrix_postgres_base_path }}/PG_VERSION"
+  register: result_pg_old_data_dir_stat
+
+- name: Warn if old Postgres data directory detected
+  debug:
+    msg: >
+      Found that you have Postgres data in `{{ matrix_postgres_base_path }}`.
+      From now on, Postgres data is supposed to be stored in `{{ matrix_postgres_data_path }}` instead.
+      We'll stop Postgres and relocate the files there for you.
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+# We should stop Postgres first, before building a list of files,
+# as to ignore any `postmaster.pid` files, etc.
+- name: Ensure matrix-postgres is stopped
+  service:
+    name: matrix-postgres
+    state: stopped
+    daemon_reload: yes
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+- name: Find files and directories in old Postgres data path
+  find:
+    paths: "{{ matrix_postgres_base_path }}"
+    file_type: any
+    excludes: ["data"]
+  register: "result_pg_old_data_dir_find"
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+- name: Ensure new Postgres data path exists
+  file:
+    path: "{{ matrix_postgres_data_path }}"
+    state: directory
+    mode: 0700
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+- block:
+    - name: Relocate Postgres data files from old directory to new
+      command: "mv {{ item.path }} {{ matrix_postgres_data_path }}/{{ item.path|basename }}"
+      with_items: "{{ result_pg_old_data_dir_find.files }}"
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+# Intentionally not starting matrix-postgres here.
+# It likely needs to be updated to point to the new directory.
+# In fact, let's even get rid of the outdated service, to ensure no one will start it
+# and have it initialize a new database.
+
+- name: Ensure outdated matrix-postgres.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-postgres.service"
+    state: absent
+  when: "result_pg_old_data_dir_stat.stat.exists"
+
+- name: Ensure systemd reloaded after getting rid of outdated matrix-postgres.service
+  service:
+    daemon_reload: yes
+  when: "result_pg_old_data_dir_stat.stat.exists"
\ No newline at end of file
diff --git a/roles/matrix-postgres/tasks/run_vacuum.yml b/roles/matrix-postgres/tasks/run_vacuum.yml
new file mode 100644
index 000000000..19a27562f
--- /dev/null
+++ b/roles/matrix-postgres/tasks/run_vacuum.yml
@@ -0,0 +1,90 @@
+---
+
+# Pre-checks
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot run vacuum."
+  when: "not matrix_postgres_enabled|bool"
+
+
+# Defaults
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+- name: Set postgres_vacuum_wait_time, if not provided
+  set_fact:
+    postgres_vacuum_wait_time: "{{ 7 * 86400 }}"
+  when: "postgres_vacuum_wait_time|default('') == ''"
+
+
+# Actual vacuuming work
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+
+- import_tasks: tasks/util/detect_existing_postgres_version.yml
+
+- name: Abort, if no existing Postgres version detected
+  fail:
+    msg: "Could not find existing Postgres installation"
+  when: "not matrix_postgres_detected_existing|bool"
+
+- name: Generate Postgres database vacuum command
+  set_fact:
+    matrix_postgres_vacuum_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-postgres-synapse-vacuum
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      {{ matrix_postgres_docker_image_latest }}
+      psql -v ON_ERROR_STOP=1 -h matrix-postgres {{ matrix_synapse_database_database }} -c 'VACUUM FULL VERBOSE'
+
+- name: Note about Postgres vacuum alternative
+  debug:
+    msg: >-
+      Running vacuum with the following Postgres command: `{{ matrix_postgres_vacuum_command }}`.
+      If this crashes, you can stop all processes (`systemctl stop matrix-*`),
+      start Postgres only (`systemctl start matrix-postgres`)
+      and manually run the above command directly on the server.
+
+- name: Populate service facts
+  service_facts:
+
+- set_fact:
+    matrix_postgres_synapse_was_running: "{{ ansible_facts.services['matrix-synapse.service']|default(none) is not none and ansible_facts.services['matrix-synapse.service'].state == 'running' }}"
+
+- name: Ensure matrix-synapse is stopped
+  service:
+    name: matrix-synapse
+    state: stopped
+    daemon_reload: yes
+
+- name: Run Postgres vacuum command
+  command: "{{ matrix_postgres_vacuum_command }}"
+  async: "{{ postgres_vacuum_wait_time }}"
+  poll: 10
+  register: matrix_postgres_synapse_vacuum_result
+
+# Intentionally show the results
+- debug: var="matrix_postgres_synapse_vacuum_result"
+
+- name: Ensure matrix-synapse is started, if it previously was
+  service:
+    name: matrix-synapse
+    state: started
+    daemon_reload: yes
+  when: "matrix_postgres_synapse_was_running|bool"
diff --git a/roles/matrix-postgres/tasks/setup_postgres.yml b/roles/matrix-postgres/tasks/setup_postgres.yml
new file mode 100644
index 000000000..4294bc113
--- /dev/null
+++ b/roles/matrix-postgres/tasks/setup_postgres.yml
@@ -0,0 +1,197 @@
+---
+
+#
+# Tasks related to setting up an internal postgres server
+#
+
+- import_tasks: "{{ role_path }}/tasks/migrate_postgres_data_directory.yml"
+  when: matrix_postgres_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/util/detect_existing_postgres_version.yml"
+  when: matrix_postgres_enabled|bool
+
+# If we have found an existing version (installed from before), we use its corresponding Docker image.
+# If not, we install using the latest Postgres.
+#
+# Upgrading is supposed to be performed separately and explicitly (see `upgrade_postgres.yml`).
+- set_fact:
+    matrix_postgres_docker_image_to_use: "{{ matrix_postgres_docker_image_latest if matrix_postgres_detected_version_corresponding_docker_image == '' else matrix_postgres_detected_version_corresponding_docker_image }}"
+  when: matrix_postgres_enabled|bool
+
+- name: Inject warning if on an old version of Postgres
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [
+          "NOTE: Your setup is on an old Postgres version ({{ matrix_postgres_docker_image_to_use }}), while {{ matrix_postgres_docker_image_latest }} is supported. You can upgrade using --tags=upgrade-postgres"
+        ]
+      }}
+  when: "matrix_postgres_enabled|bool and matrix_postgres_docker_image_to_use != matrix_postgres_docker_image_latest"
+
+# Even if we don't run the internal server, we still need this for running the CLI
+- name: Ensure postgres Docker image is pulled
+  docker_image:
+    name: "{{ matrix_postgres_docker_image_to_use }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_postgres_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_postgres_docker_image_force_pull }}"
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure Postgres paths exist
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0700
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_postgres_base_path }}"
+    - "{{ matrix_postgres_data_path }}"
+  when: matrix_postgres_enabled|bool
+
+# We do this as a separate task, because:
+# - we'd like to do it for the data path only, not for the base path (which contains root-owned environment variable files we'd like to leave as-is)
+# - we need to do it without `mode`, or we risk making certain `.conf` and other files's executable bit to flip to true
+- name: Ensure Postgres data path ownership is correct
+  file:
+    path: "{{ matrix_postgres_data_path }}"
+    state: directory
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+    recurse: yes
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure Postgres environment variables file created
+  template:
+    src: "{{ role_path }}/templates/{{ item }}.j2"
+    dest: "{{ matrix_postgres_base_path }}/{{ item }}"
+    mode: 0640
+  with_items:
+    - "env-postgres-psql"
+    - "env-postgres-server"
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure matrix-postgres-cli script created
+  template:
+    src: "{{ role_path }}/templates/usr-local-bin/matrix-postgres-cli.j2"
+    dest: "{{ matrix_local_bin_path }}/matrix-postgres-cli"
+    mode: 0755
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure matrix-change-user-admin-status script created
+  template:
+    src: "{{ role_path }}/templates/usr-local-bin/matrix-change-user-admin-status.j2"
+    dest: "{{ matrix_local_bin_path }}/matrix-change-user-admin-status"
+    mode: 0755
+  when: matrix_postgres_enabled|bool
+
+- name: (Migration) Ensure old matrix-make-user-admin script deleted
+  file:
+    path: "{{ matrix_local_bin_path }}/matrix-make-user-admin"
+    state: absent
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure matrix-postgres-update-user-password-hash script created
+  template:
+    src: "{{ role_path }}/templates/usr-local-bin/matrix-postgres-update-user-password-hash.j2"
+    dest: "{{ matrix_local_bin_path }}/matrix-postgres-update-user-password-hash"
+    mode: 0755
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure matrix-postgres.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-postgres.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-postgres.service"
+    mode: 0644
+  register: matrix_postgres_systemd_service_result
+  when: matrix_postgres_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-postgres.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_postgres_enabled|bool and matrix_postgres_systemd_service_result.changed"
+
+- include_tasks:
+    file: "{{ role_path }}/tasks/util/create_additional_databases.yml"
+    apply:
+      tags:
+        - always
+  when: "matrix_postgres_enabled|bool and matrix_postgres_additional_databases|length > 0"
+
+- name: Check existence of matrix-postgres backup data path
+  stat:
+    path: "{{ matrix_postgres_data_path }}-auto-upgrade-backup"
+  register: matrix_postgres_data_backup_path_stat
+  when: "matrix_postgres_enabled|bool"
+
+- name: Inject warning if backup data remains
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [
+          "NOTE: You have some Postgres backup data in `{{ matrix_postgres_data_path }}-auto-upgrade-backup`, which was created during the last major Postgres update you ran. If your setup works well after this upgrade, feel free to delete this whole directory."
+        ]
+      }}
+  when: "matrix_postgres_enabled|bool and matrix_postgres_data_backup_path_stat.stat.exists"
+
+
+#
+# Tasks related to getting rid of the internal postgres server (if it was previously enabled)
+#
+
+- name: Check existence of matrix-postgres service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-postgres.service"
+  register: matrix_postgres_service_stat
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Ensure matrix-postgres is stopped
+  service:
+    name: matrix-postgres
+    state: stopped
+    daemon_reload: yes
+  when: "not matrix_postgres_enabled|bool and matrix_postgres_service_stat.stat.exists"
+
+- name: Ensure matrix-postgres.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-postgres.service"
+    state: absent
+  when: "not matrix_postgres_enabled|bool and matrix_postgres_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-postgres.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_postgres_enabled|bool and matrix_postgres_service_stat.stat.exists"
+
+- name: Check existence of matrix-postgres local data path
+  stat:
+    path: "{{ matrix_postgres_data_path }}"
+  register: matrix_postgres_data_path_stat
+  when: "not matrix_postgres_enabled|bool"
+
+# We just want to notify the user. Deleting data is too destructive.
+- name: Inject warning if matrix-postgres local data remains
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [
+          "NOTE: You are not using a local PostgreSQL database, but some old data remains from before in `{{ matrix_postgres_data_path }}`. Feel free to delete it."
+        ]
+      }}
+  when: "not matrix_postgres_enabled|bool and matrix_postgres_data_path_stat.stat.exists"
+
+- name: Remove Postgres scripts
+  file:
+    path: "{{ matrix_local_bin_path }}/{{ item }}"
+    state: absent
+  with_items:
+    - matrix-postgres-cli
+    - matrix-change-user-admin-status
+    - matrix-postgres-update-user-password-hash
+  when: "not matrix_postgres_enabled|bool"
diff --git a/roles/matrix-postgres/tasks/upgrade_postgres.yml b/roles/matrix-postgres/tasks/upgrade_postgres.yml
new file mode 100644
index 000000000..564265d85
--- /dev/null
+++ b/roles/matrix-postgres/tasks/upgrade_postgres.yml
@@ -0,0 +1,172 @@
+---
+
+- name: Set default postgres_dump_dir, if not provided
+  set_fact:
+    postgres_dump_dir: "/tmp"
+  when: "postgres_dump_dir|default('') == ''"
+
+- name: Set postgres_dump_name, if not provided
+  set_fact:
+    postgres_dump_name: "matrix-postgres-dump.sql.gz"
+  when: "postgres_dump_name|default('') == ''"
+
+- name: Set postgres_auto_upgrade_backup_data_path, if not provided
+  set_fact:
+    postgres_auto_upgrade_backup_data_path: "{{ matrix_postgres_data_path }}-auto-upgrade-backup"
+  when: "postgres_auto_upgrade_backup_data_path|default('') == ''"
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+- name: Set postgres_force_upgrade, if not provided
+  set_fact:
+    postgres_force_upgrade: false
+  when: "postgres_force_upgrade|default('') == ''"
+
+- name: Fail, if trying to upgrade external Postgres database
+  fail:
+    msg: "Your configuration indicates that you're not using Postgres from this role. There is nothing to upgrade."
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Check Postgres auto-upgrade backup data directory
+  stat:
+    path: "{{ postgres_auto_upgrade_backup_data_path }}"
+  register: result_auto_upgrade_path
+
+- name: Abort, if existing Postgres auto-upgrade data path detected
+  fail:
+    msg: "Detected that a left-over {{ postgres_auto_upgrade_backup_data_path }} exists. You should rename it to {{ matrix_postgres_data_path }} if the previous upgrade went wrong, or delete it if it went well."
+  when: "result_auto_upgrade_path.stat.exists"
+
+- import_tasks: tasks/util/detect_existing_postgres_version.yml
+
+- name: Abort, if no existing Postgres version detected
+  fail:
+    msg: "Could not find existing Postgres installation"
+  when: "not matrix_postgres_detected_existing|bool"
+
+- name: Abort, if already at latest Postgres version
+  fail:
+    msg: "You are already running the latest Postgres version supported ({{ matrix_postgres_docker_image_latest }}). Nothing to do"
+  when: "matrix_postgres_detected_version_corresponding_docker_image == matrix_postgres_docker_image_latest and not postgres_force_upgrade"
+
+- debug:
+    msg: "Upgrading database from {{ matrix_postgres_detected_version_corresponding_docker_image }} to {{ matrix_postgres_docker_image_latest }}"
+
+- name: Ensure matrix-synapse is stopped
+  service:
+    name: matrix-synapse
+    state: stopped
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+
+# We dump all databases, roles, etc.
+#
+# Because we'll be importing into a new container which initializes the default
+# role (`matrix_postgres_connection_username`) and database (`matrix_postgres_db_name`) by itself on startup,
+# we need to remove these from the dump, or we'll get errors saying these already exist.
+- name: Perform Postgres database dump
+  command: >-
+    {{ matrix_host_command_docker }} run --rm --name matrix-postgres-dump
+    --log-driver=none
+    --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+    --network={{ matrix_docker_network }}
+    --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+    --entrypoint=/bin/sh
+    --mount type=bind,src={{ postgres_dump_dir }},dst=/out
+    {{ matrix_postgres_detected_version_corresponding_docker_image }}
+    -c "pg_dumpall -h matrix-postgres
+    {{ '| gzip -c ' if postgres_dump_name.endswith('.gz') else '' }}
+    > /out/{{ postgres_dump_name }}"
+
+- name: Ensure matrix-postgres is stopped
+  service:
+    name: matrix-postgres
+    state: stopped
+
+- name: Rename existing Postgres data directory
+  command: "mv {{ matrix_postgres_data_path }} {{ postgres_auto_upgrade_backup_data_path }}"
+
+- debug:
+    msg: "NOTE: Your Postgres data directory has been moved from `{{ matrix_postgres_data_path }}` to `{{ postgres_auto_upgrade_backup_data_path }}`. In the event of failure, you can move it back and run the playbook with --tags=setup-postgres to restore operation."
+
+- import_tasks: tasks/setup_postgres.yml
+
+- name: Ensure matrix-postgres autoruns and is restarted
+  service:
+    name: matrix-postgres
+    enabled: yes
+    state: restarted
+    daemon_reload: yes
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+
+# Starting the database container had automatically created the default
+# role (`matrix_postgres_connection_username`) and database (`matrix_postgres_db_name`).
+# The dump most likely contains those same entries and would try to re-create them, leading to errors.
+# We need to skip over those lines.
+- name: Generate Postgres database import command
+  set_fact:
+    matrix_postgres_import_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-postgres-import
+      --log-driver=none
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      --entrypoint=/bin/sh
+      --mount type=bind,src={{ postgres_dump_dir }},dst=/in,ro
+      {{ matrix_postgres_docker_image_latest }}
+      -c "cat /in/{{ postgres_dump_name }} |
+      {{ 'gunzip |' if postgres_dump_name.endswith('.gz') else '' }}
+      grep -vE '{{ matrix_postgres_import_roles_ignore_regex }}' |
+      grep -vE '{{ matrix_postgres_import_databases_ignore_regex }}' |
+      psql -v ON_ERROR_STOP=1 -h matrix-postgres"
+
+# This is a hack.
+# See: https://ansibledaily.com/print-to-standard-output-without-escaping/
+#
+# We want to run `debug: msg=".."`, but that dumps it as JSON and escapes double quotes within it,
+# which ruins the command (`matrix_postgres_import_command`)
+- name: Note about Postgres importing
+  set_fact:
+    dummy: true
+  with_items:
+    - >-
+        Importing Postgres database using the following command: `{{ matrix_postgres_import_command }}`.
+        If this crashes, you can stop Postgres (`systemctl stop matrix-postgres`),
+        delete the new database data (`rm -rf {{ matrix_postgres_data_path }}`)
+        and restore the automatically-made backup (`mv {{ postgres_auto_upgrade_backup_data_path }} {{ matrix_postgres_data_path }}`).
+
+- name: Perform Postgres database import
+  command: "{{ matrix_postgres_import_command }}"
+
+- name: Delete Postgres database dump file
+  file:
+    path: "{{ postgres_dump_dir }}/{{ postgres_dump_name }}"
+    state: absent
+
+- name: Ensure matrix-synapse is started
+  service:
+    name: matrix-synapse
+    state: started
+    daemon_reload: yes
+
+- debug:
+    msg: "NOTE: Your old Postgres data directory is preserved at `{{ postgres_auto_upgrade_backup_data_path }}`. You might want to get rid of it once you've confirmed that all is well."
diff --git a/roles/matrix-postgres/tasks/util/create_additional_database.yml b/roles/matrix-postgres/tasks/util/create_additional_database.yml
new file mode 100644
index 000000000..22b3c9a2a
--- /dev/null
+++ b/roles/matrix-postgres/tasks/util/create_additional_database.yml
@@ -0,0 +1,40 @@
+---
+
+# It'd be better if this is belonged to `validate_config.yml`, but it would have to be some loop-within-a-loop there,
+# and that's ugly. We also don't expect this to catch errors often. It's more of a defensive last-minute check.
+- name: Fail if additional database data appears invalid
+  fail:
+    msg: "Additional database definition ({{ additional_db }} lacks a required key: {{ item }}"
+  when: "item not in additional_db"
+  with_items: "{{ ['name', 'username', 'password'] }}"
+
+# The SQL statements that we'll run against Postgres are stored in a file that others can't read.
+# This file will be mounted into the container and fed to Postgres.
+# This way, we avoid passing sensitive data around in CLI commands that other users on the system can see.
+- name: Create additional database initialization SQL file for {{ additional_db.name }}
+  template:
+    src: "{{ role_path }}/templates/sql/init-additional-db-user-and-role.sql.j2"
+    dest: "/tmp/matrix-postgres-init-additional-db-user-and-role.sql"
+    mode: 0600
+    owner: "{{ matrix_user_uid }}"
+    group: "{{ matrix_user_gid }}"
+
+- name: Execute Postgres additional database initialization SQL file for {{ additional_db.name }}
+  command:
+    cmd: >-
+      {{ matrix_host_command_docker }} run
+      --rm
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      --network {{ matrix_docker_network }}
+      --mount type=bind,src=/tmp/matrix-postgres-init-additional-db-user-and-role.sql,dst=/matrix-postgres-init-additional-db-user-and-role.sql,ro
+      --entrypoint=/bin/sh
+      {{ matrix_postgres_docker_image_to_use }}
+      -c
+      'psql -h {{ matrix_postgres_connection_hostname }} --file=/matrix-postgres-init-additional-db-user-and-role.sql'
+
+- name: Delete additional database initialization SQL file for {{ additional_db.name }}
+  file:
+    path: /tmp/matrix-postgres-init-additional-db-user-and-role.sql
+    state: absent
diff --git a/roles/matrix-postgres/tasks/util/create_additional_databases.yml b/roles/matrix-postgres/tasks/util/create_additional_databases.yml
new file mode 100644
index 000000000..0ad460ddd
--- /dev/null
+++ b/roles/matrix-postgres/tasks/util/create_additional_databases.yml
@@ -0,0 +1,23 @@
+---
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+  register: matrix_postgres_service_start_result
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ matrix_postgres_additional_databases_postgres_start_wait_timeout_seconds }}"
+  delegate_to: 127.0.0.1
+  become: false
+  when: "matrix_postgres_service_start_result.changed|bool"
+
+- name: Create additional Postgres user and database
+  include_tasks: "{{ role_path }}/tasks/util/create_additional_database.yml"
+  with_items: "{{ matrix_postgres_additional_databases }}"
+  loop_control:
+    loop_var: additional_db
+  # Suppress logging to avoid dumping the credentials to the shell
+  no_log: true
diff --git a/roles/matrix-postgres/tasks/util/detect_existing_postgres_version.yml b/roles/matrix-postgres/tasks/util/detect_existing_postgres_version.yml
new file mode 100644
index 000000000..9032c15e0
--- /dev/null
+++ b/roles/matrix-postgres/tasks/util/detect_existing_postgres_version.yml
@@ -0,0 +1,56 @@
+---
+
+# This utility aims to determine if there is some existing Postgres version in use or not.
+# If there is, it also tries to detect the Docker image that corresponds to that version.
+
+- name: Initialize Postgres version determination variables (default to empty)
+  set_fact:
+    matrix_postgres_detection_pg_version_path: "{{ matrix_postgres_data_path }}/PG_VERSION"
+    matrix_postgres_detected_existing: false
+    matrix_postgres_detected_version: ""
+    matrix_postgres_detected_version_corresponding_docker_image: ""
+
+- name: Determine existing Postgres version (check PG_VERSION file)
+  stat:
+    path: "{{ matrix_postgres_detection_pg_version_path }}"
+  register: result_pg_version_stat
+
+- set_fact:
+    matrix_postgres_detected_existing: true
+  when: "result_pg_version_stat.stat.exists"
+
+- name: Determine existing Postgres version (read PG_VERSION file)
+  slurp:
+    src: "{{ matrix_postgres_detection_pg_version_path }}"
+  register: result_pg_version
+  when: matrix_postgres_detected_existing|bool
+
+- name: Determine existing Postgres version (make sense of PG_VERSION file)
+  set_fact:
+    matrix_postgres_detected_version: "{{ result_pg_version['content']|b64decode|replace('\n', '') }}"
+  when: matrix_postgres_detected_existing|bool
+
+- name: Determine corresponding Docker image to detected version (assume default of latest)
+  set_fact:
+    matrix_postgres_detected_version_corresponding_docker_image: "{{ matrix_postgres_docker_image_latest }}"
+  when: "matrix_postgres_detected_version != ''"
+
+- name: Determine corresponding Docker image to detected version (use 9.x, if detected)
+  set_fact:
+    matrix_postgres_detected_version_corresponding_docker_image: "{{ matrix_postgres_docker_image_v9 }}"
+  when: "matrix_postgres_detected_version.startswith('9.')"
+
+- name: Determine corresponding Docker image to detected version (use 10.x, if detected)
+  set_fact:
+    matrix_postgres_detected_version_corresponding_docker_image: "{{ matrix_postgres_docker_image_v10 }}"
+  when: "matrix_postgres_detected_version == '10' or matrix_postgres_detected_version.startswith('10.')"
+
+- name: Determine corresponding Docker image to detected version (use 11.x, if detected)
+  set_fact:
+    matrix_postgres_detected_version_corresponding_docker_image: "{{ matrix_postgres_docker_image_v11 }}"
+  when: "matrix_postgres_detected_version == '11' or matrix_postgres_detected_version.startswith('11.')"
+
+- name: Determine corresponding Docker image to detected version (use 12.x, if detected)
+  set_fact:
+    matrix_postgres_detected_version_corresponding_docker_image: "{{ matrix_postgres_docker_image_v12 }}"
+  when: "matrix_postgres_detected_version == '12' or matrix_postgres_detected_version.startswith('12.')"
diff --git a/roles/matrix-postgres/tasks/util/migrate_db_to_postgres.yml b/roles/matrix-postgres/tasks/util/migrate_db_to_postgres.yml
new file mode 100644
index 000000000..cf595ade2
--- /dev/null
+++ b/roles/matrix-postgres/tasks/util/migrate_db_to_postgres.yml
@@ -0,0 +1,169 @@
+---
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot migrate."
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Fail if util called incorrectly (missing matrix_postgres_db_migration_request)
+  fail:
+    msg: "The `matrix_postgres_db_migration_request` variable needs to be provided to this util."
+  when: "matrix_postgres_db_migration_request is not defined"
+
+- name: Fail if util called incorrectly (invalid matrix_postgres_db_migration_request)
+  fail:
+    msg: "The `matrix_postgres_db_migration_request` variable needs to contain `{{ item }}`."
+  with_items:
+    - src
+    - dst
+    - caller
+    - engine_variable_name
+    - systemd_services_to_stop
+  when: "item not in matrix_postgres_db_migration_request"
+
+- name: Check if the provided source database file exists
+  stat:
+    path: "{{ matrix_postgres_db_migration_request.src }}"
+  register: matrix_postgres_db_migration_request_src_stat_result
+
+- name: Fail if provided source database file doesn't exist
+  fail:
+    msg: "File cannot be found on the server at {{ matrix_postgres_db_migration_request.src }}"
+  when: "not matrix_postgres_db_migration_request_src_stat_result.stat.exists"
+
+- block:
+    - name: Ensure pgloader repository is present on self-build
+      git:
+        repo: "{{ matrix_postgres_pgloader_container_image_self_build_repo }}"
+        dest: "{{ matrix_postgres_pgloader_container_image_self_build_src_path }}"
+        version: "{{ matrix_postgres_pgloader_container_image_self_build_repo_branch }}"
+        force: "yes"
+      register: matrix_postgres_pgloader_git_pull_results
+
+    # If `stable` is used, we hit an error when processing /opt/src/pgloader/build/quicklisp/dists/quicklisp/software/uax-15-20201220-git/data/CompositionExclusions.txt:
+    # > the octet sequence #(194) cannot be decoded
+    #
+    # The issue is described here and is not getting fixed for months: https://github.com/dimitri/pgloader/pull/1179
+    #
+    # Although we're not using the dimitri/pgloader image, the one we're using suffers from the same problem.
+    - name: Switch pgloader base image from Debian stable (likely 10.x/Buster) to Bullseye
+      lineinfile:
+        path: "{{ matrix_postgres_pgloader_container_image_self_build_src_path }}/Dockerfile"
+        regexp: "{{ item.match }}"
+        line: "{{ item.replace }}"
+      with_items:
+        - match: '^FROM debian:stable-slim as builder$'
+          replace: 'FROM debian:bullseye-slim as builder'
+        - match: '^FROM debian:stable-slim$'
+          replace: 'FROM debian:bullseye-slim'
+
+    - name: Ensure pgloader Docker image is built
+      docker_image:
+        name: "{{ matrix_postgres_pgloader_docker_image }}"
+        source: build
+        force_source: "{{ matrix_postgres_pgloader_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+        force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_postgres_pgloader_git_pull_results.changed }}"
+        build:
+          dockerfile: Dockerfile
+          path: "{{ matrix_postgres_pgloader_container_image_self_build_src_path }}"
+          pull: yes
+  when: "matrix_postgres_pgloader_container_image_self_build|bool"
+
+- name: Ensure pgloader Docker image is pulled
+  docker_image:
+    name: "{{ matrix_postgres_pgloader_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_postgres_pgloader_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_postgres_pgloader_docker_image_force_pull }}"
+  when: "not matrix_postgres_pgloader_container_image_self_build"
+
+# Defaults
+
+- name: Set postgres_start_wait_time, if not provided
+  set_fact:
+    postgres_start_wait_time: 15
+  when: "postgres_start_wait_time|default('') == ''"
+
+# Actual import work
+
+# matrix-postgres is most likely started already
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+  register: matrix_postgres_service_start_result
+
+- name: Wait a bit, so that Postgres can start
+  wait_for:
+    timeout: "{{ postgres_start_wait_time }}"
+  delegate_to: 127.0.0.1
+  become: false
+  when: "matrix_postgres_service_start_result.changed|bool"
+
+# We only stop services here, leaving it to the caller to start them later.
+#
+# We can't start them, because they probably need to be reconfigured too (changing the configuration from using SQLite to Postgres, etc.),
+# before starting.
+#
+# Since the caller will be starting them, it might make sense to leave stopping to it as well.
+# However, we don't do it, because it's simpler having it here, and it also gets to happen only if we'll be doing an import.
+# If we bailed out (somewhere above), nothing would have gotten stopped. It's nice to leave this running in such cases.
+- name: Ensure systemd services blocking the database import are stopped
+  service:
+    name: "{{ item }}"
+    state: stopped
+  failed_when: false
+  with_items: "{{ matrix_postgres_db_migration_request.systemd_services_to_stop }}"
+
+- name: Import {{ matrix_postgres_db_migration_request.engine_old }} database from {{ matrix_postgres_db_migration_request.src }} into Postgres
+  command:
+    cmd: >-
+      {{ matrix_host_command_docker }} run
+      --rm
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --mount type=bind,src={{ matrix_postgres_db_migration_request.src }},dst=/in.db,ro
+      --entrypoint=/bin/sh
+      {{ matrix_postgres_pgloader_docker_image }}
+      -c
+      'pgloader {{ matrix_postgres_db_migration_request.pgloader_options|default([])|join(' ') }} /in.db {{ matrix_postgres_db_migration_request.dst }}'
+
+- block:
+    # We can't use `{{ role_path }}` here, neither with `import_tasks`, nor with `include_tasks`,
+    # because it refers to the role that included this util, and not to the role this file belongs to.
+    - import_tasks: "{{ role_path }}/../matrix-postgres/tasks/util/detect_existing_postgres_version.yml"
+
+    - set_fact:
+        matrix_postgres_docker_image_to_use: "{{ matrix_postgres_docker_image_latest if matrix_postgres_detected_version_corresponding_docker_image == '' else matrix_postgres_detected_version_corresponding_docker_image }}"
+
+    - name: Execute additional Postgres SQL migration statements
+      command:
+        cmd: >-
+          {{ matrix_host_command_docker }} run
+          --rm
+          --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+          --cap-drop=ALL
+          --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+          --network={{ matrix_docker_network }}
+          {{ matrix_postgres_docker_image_to_use }}
+          psql --host=matrix-postgres --dbname={{ matrix_postgres_db_migration_request.additional_psql_statements_db_name }} --command='{{ item }}'
+      with_items: "{{ matrix_postgres_db_migration_request.additional_psql_statements_list }}"
+
+  when: "matrix_postgres_db_migration_request.additional_psql_statements_list|default([])|length > 0"
+
+- name: Archive {{ matrix_postgres_db_migration_request.engine_old }} database ({{ matrix_postgres_db_migration_request.src }} -> {{ matrix_postgres_db_migration_request.src }}.backup)
+  command:
+    cmd: "mv {{ matrix_postgres_db_migration_request.src }} {{ matrix_postgres_db_migration_request.src }}.backup"
+
+- name: Inject result
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [
+          "NOTE: Your {{ matrix_postgres_db_migration_request.engine_old }} database file has been imported into Postgres. The original database file has been moved from `{{ matrix_postgres_db_migration_request.src }}` to `{{ matrix_postgres_db_migration_request.src }}.backup`. When you've confirmed that the import went well and everything works, you should be able to safely delete this file."
+        ]
+      }}
diff --git a/roles/matrix-postgres/tasks/validate_config.yml b/roles/matrix-postgres/tasks/validate_config.yml
new file mode 100644
index 000000000..eac4dd5b1
--- /dev/null
+++ b/roles/matrix-postgres/tasks/validate_config.yml
@@ -0,0 +1,39 @@
+---
+
+- name: (Deprecation) Warn about matrix_postgres_use_external usage
+  fail:
+    msg: >
+      The `matrix_postgres_use_external` variable defined in your configuration is not used by this playbook anymore!
+      You'll need to adapt to the new way of using an external Postgres server.
+      It's a combination of `matrix_postgres_enabled: false` and specifying Postgres connection
+      details in a few `matrix_postgres_connection_` variables.
+      See the "Using an external PostgreSQL server (optional)" documentation page.
+  when: "'matrix_postgres_use_external' in vars"
+
+# This is separate (from the other required variables below),
+# because we'd like to have a friendlier message for our existing users.
+- name: Fail if matrix_postgres_connection_password not defined
+  fail:
+    msg: >-
+      The playbook no longer has a default Postgres password defined in the `matrix_postgres_connection_password` variable, among lots of other Postgres changes.
+      You need to perform multiple manual steps to resolve this.
+      See our changelog for more details:
+      https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/CHANGELOG.md#breaking-change-postgres-changes-that-require-manual-intervention
+  when: "matrix_postgres_connection_password == ''"
+
+- name: Fail if required Postgres settings not defined
+  fail:
+    msg: >-
+      You need to define a required configuration setting (`{{ item }}`).
+  when: "vars[item] == ''"
+  with_items:
+    - "matrix_postgres_connection_hostname"
+    - "matrix_postgres_connection_port"
+    - "matrix_postgres_connection_username"
+    - "matrix_postgres_connection_password"
+    - "matrix_postgres_db_name"
+
+- name: Fail if Postgres password length exceeded
+  fail:
+    msg: "The maximum `matrix_postgres_connection_password` length is 99 characters"
+  when: "matrix_postgres_connection_password|length > 99"
diff --git a/roles/matrix-postgres/templates/env-postgres-psql.j2 b/roles/matrix-postgres/templates/env-postgres-psql.j2
new file mode 100644
index 000000000..c61927a3e
--- /dev/null
+++ b/roles/matrix-postgres/templates/env-postgres-psql.j2
@@ -0,0 +1,4 @@
+#jinja2: lstrip_blocks: "True"
+PGUSER={{ matrix_postgres_connection_username }}
+PGPASSWORD={{ matrix_postgres_connection_password }}
+PGDATABASE={{ matrix_postgres_db_name }}
\ No newline at end of file
diff --git a/roles/matrix-postgres/templates/env-postgres-server.j2 b/roles/matrix-postgres/templates/env-postgres-server.j2
new file mode 100644
index 000000000..06feb82a6
--- /dev/null
+++ b/roles/matrix-postgres/templates/env-postgres-server.j2
@@ -0,0 +1,7 @@
+#jinja2: lstrip_blocks: "True"
+POSTGRES_USER={{ matrix_postgres_connection_username }}
+POSTGRES_PASSWORD={{ matrix_postgres_connection_password }}
+POSTGRES_DB={{ matrix_postgres_db_name }}
+# Synapse refuses to run if collation is not C.
+# See https://github.com/matrix-org/synapse/issues/6722
+POSTGRES_INITDB_ARGS=--lc-collate C --lc-ctype C --encoding UTF8
diff --git a/roles/matrix-postgres/templates/sql/init-additional-db-user-and-role.sql.j2 b/roles/matrix-postgres/templates/sql/init-additional-db-user-and-role.sql.j2
new file mode 100644
index 000000000..a5a3385b6
--- /dev/null
+++ b/roles/matrix-postgres/templates/sql/init-additional-db-user-and-role.sql.j2
@@ -0,0 +1,19 @@
+-- `CREATE USER` does not support `IF NOT EXISTS`, so we use this workaround to prevent an error and raise a notice instead.
+-- Seen here: https://stackoverflow.com/a/49858797
+DO $$
+BEGIN
+  CREATE USER "{{ additional_db.username }}";
+  EXCEPTION WHEN DUPLICATE_OBJECT THEN
+  RAISE NOTICE 'not creating user "{{ additional_db.username }}", since it already exists';
+END
+$$;
+
+-- This is useful for initial user creation (since we don't assign a password above) and for handling subsequent password changes
+-- TODO - we should escape quotes in the password.
+ALTER ROLE "{{ additional_db.username }}" PASSWORD '{{ additional_db.password }}';
+
+-- This will generate an error on subsequent execution
+CREATE DATABASE "{{ additional_db.name }}" WITH LC_CTYPE 'C' LC_COLLATE 'C' OWNER "{{ additional_db.username }}";
+
+-- This is useful for changing the database owner subsequently
+ALTER DATABASE "{{ additional_db.name }}" OWNER TO "{{ additional_db.username }}";
diff --git a/roles/matrix-postgres/templates/systemd/matrix-postgres.service.j2 b/roles/matrix-postgres/templates/systemd/matrix-postgres.service.j2
new file mode 100644
index 000000000..6d1b1c6ff
--- /dev/null
+++ b/roles/matrix-postgres/templates/systemd/matrix-postgres.service.j2
@@ -0,0 +1,41 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Postgres server
+After=docker.service
+Requires=docker.service
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-postgres 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-postgres 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-postgres \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,noexec,nosuid,size=100m \
+			--tmpfs=/run/postgresql:rw,noexec,nosuid,size=100m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_postgres_container_postgres_bind_port %}
+			-p {{ matrix_postgres_container_postgres_bind_port }}:5432 \
+			{% endif %}
+			--env-file={{ matrix_postgres_base_path }}/env-postgres-server \
+			--mount type=bind,src={{ matrix_postgres_data_path }},dst=/var/lib/postgresql/data \
+			--mount type=bind,src=/etc/passwd,dst=/etc/passwd,ro \
+			{% for arg in matrix_postgres_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_postgres_docker_image_to_use }} \
+			postgres {{ matrix_postgres_process_extra_arguments|join(' ') }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-postgres 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-postgres 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-postgres
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-postgres/templates/usr-local-bin/matrix-change-user-admin-status.j2 b/roles/matrix-postgres/templates/usr-local-bin/matrix-change-user-admin-status.j2
new file mode 100644
index 000000000..6c3082ef4
--- /dev/null
+++ b/roles/matrix-postgres/templates/usr-local-bin/matrix-change-user-admin-status.j2
@@ -0,0 +1,19 @@
+#jinja2: lstrip_blocks: "True"
+#!/bin/bash
+
+if [ $# -ne 2 ]; then
+	echo "Usage: "$0" <username> <0/1>"
+	echo "Usage: 0 = non-admin"
+	echo "Usage: 1 = admin"
+	exit 1
+fi
+
+docker run \
+	-it \
+	--rm \
+	--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+	--cap-drop=ALL \
+	--env-file={{ matrix_postgres_base_path }}/env-postgres-psql \
+	--network {{ matrix_docker_network }} \
+	{{ matrix_postgres_docker_image_to_use }} \
+	psql -h {{ matrix_postgres_connection_hostname }} --dbname={{ matrix_synapse_database_database }} -c "UPDATE users set admin=$2 WHERE name like '@$1:{{ matrix_domain }}'"
diff --git a/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-cli.j2 b/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-cli.j2
new file mode 100644
index 000000000..de09a4eb2
--- /dev/null
+++ b/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-cli.j2
@@ -0,0 +1,13 @@
+#jinja2: lstrip_blocks: "True"
+#!/bin/bash
+
+docker run \
+	-it \
+	--rm \
+	--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+	--cap-drop=ALL \
+	--env-file={{ matrix_postgres_base_path }}/env-postgres-psql \
+	--network {{ matrix_docker_network }} \
+	{{ matrix_postgres_docker_image_to_use }} \
+	psql -h {{ matrix_postgres_connection_hostname }} \
+	"$@"
diff --git a/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-update-user-password-hash.j2 b/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-update-user-password-hash.j2
new file mode 100644
index 000000000..0fbf4f21b
--- /dev/null
+++ b/roles/matrix-postgres/templates/usr-local-bin/matrix-postgres-update-user-password-hash.j2
@@ -0,0 +1,16 @@
+#jinja2: lstrip_blocks: "True"
+#!/bin/bash
+
+if [ $# -ne 2 ]; then
+	echo "Usage: "$0" <username> <password_hash>"
+	exit 1
+fi
+
+docker run \
+	--rm \
+	--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+	--cap-drop=ALL \
+	--env-file={{ matrix_postgres_base_path }}/env-postgres-psql \
+	--network {{ matrix_docker_network }} \
+	{{ matrix_postgres_docker_image_to_use }} \
+	psql -h {{ matrix_postgres_connection_hostname }} --dbname={{ matrix_synapse_database_database }} -c "UPDATE users set password_hash='$2' WHERE name = '@$1:{{ matrix_domain }}'"
diff --git a/roles/matrix-prometheus-node-exporter/defaults/main.yml b/roles/matrix-prometheus-node-exporter/defaults/main.yml
new file mode 100644
index 000000000..492d48b19
--- /dev/null
+++ b/roles/matrix-prometheus-node-exporter/defaults/main.yml
@@ -0,0 +1,34 @@
+# matrix-prometheus-node-exporter is an Prometheus exporter for machine metrics
+# See: https://prometheus.io/docs/guides/node-exporter/
+
+matrix_prometheus_node_exporter_enabled: false
+
+matrix_prometheus_node_exporter_version: v1.1.2
+matrix_prometheus_node_exporter_docker_image: "{{ matrix_container_global_registry_prefix }}prom/node-exporter:{{ matrix_prometheus_node_exporter_version }}"
+matrix_prometheus_node_exporter_docker_image_force_pull: "{{ matrix_prometheus_node_exporter_docker_image.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_prometheus_node_exporter_container_extra_arguments: []
+
+# List of systemd services that matrix-prometheus.service depends on
+matrix_prometheus_node_exporter_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-prometheus.service wants
+matrix_prometheus_node_exporter_systemd_wanted_services_list: []
+
+# Controls whether the matrix-prometheus container exposes its HTTP port (tcp/9100 in the container).
+#
+# Takes an "<ip>:<port>" value (e.g. "127.0.0.1:9100"), or empty string to not expose.
+#
+# Official recommendations are to run this container with `--net=host`,
+# but we don't do that, since it:
+# - likely exposes the metrics web server way too publicly (before applying https://github.com/spantaleev/matrix-docker-ansible-deploy/pull/1008)
+# - or listens on a loopback interface only (--net=host and 127.0.0.1:9100), which is not reachable from another container (like `matrix-prometheus`)
+#
+# Using `--net=host` and binding to Docker's `matrix` bridge network may be a solution to both,
+# but that's trickier to accomplish and won't necessarily work (hasn't been tested).
+#
+# Not using `--net=host` means that our network statistic reports are likely broken (inaccurate),
+# because node-exporter can't see all interfaces, etc.
+# For now, we'll live with that, until someone develops a better solution.
+matrix_prometheus_node_exporter_container_http_host_bind_port: ''
diff --git a/roles/matrix-prometheus-node-exporter/tasks/init.yml b/roles/matrix-prometheus-node-exporter/tasks/init.yml
new file mode 100644
index 000000000..2894b7176
--- /dev/null
+++ b/roles/matrix-prometheus-node-exporter/tasks/init.yml
@@ -0,0 +1,5 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-prometheus-node-exporter.service'] }}"
+  when: matrix_prometheus_node_exporter_enabled|bool
+
+
diff --git a/roles/matrix-prometheus-node-exporter/tasks/main.yml b/roles/matrix-prometheus-node-exporter/tasks/main.yml
new file mode 100644
index 000000000..172b57215
--- /dev/null
+++ b/roles/matrix-prometheus-node-exporter/tasks/main.yml
@@ -0,0 +1,8 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup.yml"
+  tags:
+    - setup-all
+    - setup-prometheus-node-exporter
diff --git a/roles/matrix-prometheus-node-exporter/tasks/setup.yml b/roles/matrix-prometheus-node-exporter/tasks/setup.yml
new file mode 100644
index 000000000..34086e6cf
--- /dev/null
+++ b/roles/matrix-prometheus-node-exporter/tasks/setup.yml
@@ -0,0 +1,54 @@
+---
+
+#
+# Tasks related to setting up matrix-prometheus-node-exporter
+#
+
+- name: Ensure matrix-prometheus-node-exporter image is pulled
+  docker_image:
+    name: "{{ matrix_prometheus_node_exporter_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_prometheus_node_exporter_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_prometheus_node_exporter_docker_image_force_pull }}"
+  when: "matrix_prometheus_node_exporter_enabled|bool"
+
+- name: Ensure matrix-prometheus-node-exporter.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-prometheus-node-exporter.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-prometheus-node-exporter.service"
+    mode: 0644
+  register: matrix_prometheus_node_exporter_systemd_service_result
+  when: matrix_prometheus_node_exporter_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-prometheus.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_prometheus_node_exporter_enabled|bool and matrix_prometheus_node_exporter_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of matrix-prometheus-node-exporter (if it was previously enabled)
+#
+
+- name: Check existence of matrix-prometheus-node-exporter service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus-node-exporter.service"
+  register: matrix_prometheus_node_exporter_service_stat
+
+- name: Ensure matrix-prometheus-node-exporter is stopped
+  service:
+    name: matrix-prometheus-node-exporter
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_prometheus_node_exporter_enabled|bool and matrix_prometheus_node_exporter_service_stat.stat.exists"
+
+- name: Ensure matrix-prometheus-node-exporter.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus-node-exporter.service"
+    state: absent
+  when: "not matrix_prometheus_node_exporter_enabled|bool and matrix_prometheus_node_exporter_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-prometheus-node-exporter.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_prometheus_node_exporter_enabled|bool and matrix_prometheus_node_exporter_service_stat.stat.exists"
diff --git a/roles/matrix-prometheus-node-exporter/templates/systemd/matrix-prometheus-node-exporter.service.j2 b/roles/matrix-prometheus-node-exporter/templates/systemd/matrix-prometheus-node-exporter.service.j2
new file mode 100644
index 000000000..210a0d97a
--- /dev/null
+++ b/roles/matrix-prometheus-node-exporter/templates/systemd/matrix-prometheus-node-exporter.service.j2
@@ -0,0 +1,44 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-prometheus-node-exporter
+{% for service in matrix_prometheus_node_exporter_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_prometheus_node_exporter_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus-node-exporter 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus-node-exporter 2>/dev/null'
+
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-prometheus-node-exporter \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			{% for arg in matrix_prometheus_node_exporter_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			--network={{ matrix_docker_network }} \
+			{% if matrix_prometheus_node_exporter_container_http_host_bind_port %}
+			-p {{ matrix_prometheus_node_exporter_container_http_host_bind_port }}:9100 \
+			{% endif %}
+			--pid=host \
+			--mount type=bind,src=/,dst=/host,ro,bind-propagation=rslave \
+			{{ matrix_prometheus_node_exporter_docker_image }} \
+			--path.rootfs=/host
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus-node-exporter 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus-node-exporter 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-prometheus-node-exporter
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-prometheus-postgres-exporter/defaults/main.yml b/roles/matrix-prometheus-postgres-exporter/defaults/main.yml
new file mode 100644
index 000000000..8aca45762
--- /dev/null
+++ b/roles/matrix-prometheus-postgres-exporter/defaults/main.yml
@@ -0,0 +1,49 @@
+# matrix-prometheus-postgres-exporter is an Prometheus exporter for postgres metrics
+# See: https://github.com/prometheus-community/postgres_exporter
+
+matrix_prometheus_postgres_exporter_enabled: false
+
+matrix_prometheus_postgres_exporter_version: v0.9.0
+matrix_prometheus_postgres_exporter_port: 9187
+
+matrix_prometheus_postgres_exporter_docker_image: "quay.io/prometheuscommunity/postgres-exporter:{{ matrix_prometheus_postgres_exporter_version }}"
+matrix_prometheus_postgres_exporter_docker_image_force_pull: "{{ matrix_prometheus_postgres_exporter_docker_image.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_prometheus_postgres_exporter_container_extra_arguments: ["-e PG_EXPORTER_AUTO_DISCOVER_DATABASES=true",
+ "-e PG_EXPORTER_WEB_LISTEN_ADDRESS=\":{{matrix_prometheus_postgres_exporter_port}}\"",
+ "-e DATA_SOURCE_NAME=\"postgresql://{{matrix_prometheus_postgres_exporter_database_username}}:{{matrix_prometheus_postgres_exporter_database_password}}@{{matrix_prometheus_postgres_exporter_database_hostname}}:5432/{{matrix_prometheus_postgres_exporter_database_name}}?sslmode=disable\"" ]
+
+# List of systemd services that matrix-prometheus-postgres-exporter.service depends on
+matrix_prometheus_postgres_exporter_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-prometheus-postgres-exporter.service wants
+matrix_prometheus_postgres_exporter_systemd_wanted_services_list: []
+
+# details for connecting to the database
+matrix_prometheus_postgres_exporter_database_username: 'matrix_prometheus_postgres_exporter'
+matrix_prometheus_postgres_exporter_database_password: 'some-password'
+matrix_prometheus_postgres_exporter_database_hostname: 'matrix-postgres'
+matrix_prometheus_postgres_exporter_database_port: 5432
+matrix_prometheus_postgres_exporter_database_name: 'matrix_prometheus_postgres_exporter'
+
+
+# Controls whether the matrix-prometheus container exposes its HTTP port (tcp/9100 in the container).
+#
+# Takes an "<ip>:<port>" value (e.g. "127.0.0.1:9100"), or empty string to not expose.
+#
+# Official recommendations are to run this container with `--net=host`,
+# but we don't do that, since it:
+# - likely exposes the metrics web server way too publicly (before applying https://github.com/spantaleev/matrix-docker-ansible-deploy/pull/1008)
+# - or listens on a loopback interface only (--net=host and 127.0.0.1:9100), which is not reachable from another container (like `matrix-prometheus`)
+#
+# Using `--net=host` and binding to Docker's `matrix` bridge network may be a solution to both,
+# but that's trickier to accomplish and won't necessarily work (hasn't been tested).
+#
+# Not using `--net=host` means that our network statistic reports are likely broken (inaccurate),
+# because node-exporter can't see all interfaces, etc.
+# For now, we'll live with that, until someone develops a better solution.
+matrix_prometheus_postgres_exporter_container_http_host_bind_port: ''
+
+matrix_prometheus_postgres_exporter_dashboard_urls:
+- "https://grafana.com/api/dashboards/9628/revisions/7/download"
\ No newline at end of file
diff --git a/roles/matrix-prometheus-postgres-exporter/tasks/init.yml b/roles/matrix-prometheus-postgres-exporter/tasks/init.yml
new file mode 100644
index 000000000..2bd6904ec
--- /dev/null
+++ b/roles/matrix-prometheus-postgres-exporter/tasks/init.yml
@@ -0,0 +1,5 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-prometheus-postgres-exporter.service'] }}"
+  when: matrix_prometheus_postgres_exporter_enabled|bool
+
+
diff --git a/roles/matrix-prometheus-postgres-exporter/tasks/main.yml b/roles/matrix-prometheus-postgres-exporter/tasks/main.yml
new file mode 100644
index 000000000..e3c364fa9
--- /dev/null
+++ b/roles/matrix-prometheus-postgres-exporter/tasks/main.yml
@@ -0,0 +1,8 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup.yml"
+  tags:
+    - setup-all
+    - setup-prometheus-postgres-exporter
diff --git a/roles/matrix-prometheus-postgres-exporter/tasks/setup.yml b/roles/matrix-prometheus-postgres-exporter/tasks/setup.yml
new file mode 100644
index 000000000..076ece1a8
--- /dev/null
+++ b/roles/matrix-prometheus-postgres-exporter/tasks/setup.yml
@@ -0,0 +1,54 @@
+---
+
+#
+# Tasks related to setting up matrix-prometheus-postgres-exporter
+#
+
+- name: Ensure matrix-prometheus-postgres-exporter image is pulled
+  docker_image:
+    name: "{{ matrix_prometheus_postgres_exporter_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_prometheus_postgres_exporter_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_prometheus_postgres_exporter_docker_image_force_pull }}"
+  when: "matrix_prometheus_postgres_exporter_enabled|bool"
+
+- name: Ensure matrix-prometheus-postgres-exporter.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-prometheus-postgres-exporter.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-prometheus-postgres-exporter.service"
+    mode: 0644
+  register: matrix_prometheus_postgres_exporter_systemd_service_result
+  when: matrix_prometheus_postgres_exporter_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-prometheus.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_prometheus_postgres_exporter_enabled|bool and matrix_prometheus_postgres_exporter_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of matrix-prometheus-postgres-exporter (if it was previously enabled)
+#
+
+- name: Check existence of matrix-prometheus-postgres-exporter service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus-postgres-exporter.service"
+  register: matrix_prometheus_postgres_exporter_service_stat
+
+- name: Ensure matrix-prometheus-postgres-exporter is stopped
+  service:
+    name: matrix-prometheus-postgres-exporter
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_prometheus_postgres_exporter_enabled|bool and matrix_prometheus_postgres_exporter_service_stat.stat.exists"
+
+- name: Ensure matrix-prometheus-postgres-exporter.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus-postgres-exporter.service"
+    state: absent
+  when: "not matrix_prometheus_postgres_exporter_enabled|bool and matrix_prometheus_postgres_exporter_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-prometheus-postgres-exporter.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_prometheus_postgres_exporter_enabled|bool and matrix_prometheus_postgres_exporter_service_stat.stat.exists"
diff --git a/roles/matrix-prometheus-postgres-exporter/templates/systemd/matrix-prometheus-postgres-exporter.service.j2 b/roles/matrix-prometheus-postgres-exporter/templates/systemd/matrix-prometheus-postgres-exporter.service.j2
new file mode 100644
index 000000000..b25cb5ded
--- /dev/null
+++ b/roles/matrix-prometheus-postgres-exporter/templates/systemd/matrix-prometheus-postgres-exporter.service.j2
@@ -0,0 +1,42 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-prometheus-postgres-exporter
+{% for service in matrix_prometheus_postgres_exporter_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_prometheus_postgres_exporter_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus-postgres-exporter 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus-postgres-exporter 2>/dev/null'
+
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-prometheus-postgres-exporter \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			{% for arg in matrix_prometheus_postgres_exporter_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			--network={{ matrix_docker_network }} \
+			{% if matrix_prometheus_postgres_exporter_container_http_host_bind_port %}
+			-p {{ matrix_prometheus_postgres_exporter_container_http_host_bind_port }}:{{matrix_prometheus_postgres_exporter_port}} \
+			{% endif %}
+			--pid=host \
+			{{ matrix_prometheus_postgres_exporter_docker_image }} 
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus-postgres-exporter 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus-postgres-exporter 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-prometheus-postgres-exporter
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-prometheus/defaults/main.yml b/roles/matrix-prometheus/defaults/main.yml
new file mode 100644
index 000000000..3725993cb
--- /dev/null
+++ b/roles/matrix-prometheus/defaults/main.yml
@@ -0,0 +1,67 @@
+# matrix-prometheus is an open-source systems monitoring and alerting toolkit
+# See: https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md
+
+matrix_prometheus_enabled: false
+
+matrix_prometheus_version: v2.28.1
+matrix_prometheus_docker_image: "{{ matrix_container_global_registry_prefix }}prom/prometheus:{{ matrix_prometheus_version }}"
+matrix_prometheus_docker_image_force_pull: "{{ matrix_prometheus_docker_image.endswith(':latest') }}"
+
+matrix_prometheus_base_path: "{{ matrix_base_data_path }}/prometheus"
+matrix_prometheus_config_path: "{{ matrix_prometheus_base_path }}/config"
+matrix_prometheus_data_path: "{{ matrix_prometheus_base_path }}/data"
+
+# A list of extra arguments to pass to the container
+matrix_prometheus_container_extra_arguments: []
+
+# List of systemd services that matrix-prometheus.service depends on
+matrix_prometheus_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-prometheus.service wants
+matrix_prometheus_systemd_wanted_services_list: []
+
+# Controls whether the matrix-prometheus container exposes its HTTP port (tcp/9090 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:9090"), or empty string to not expose.
+matrix_prometheus_container_http_host_bind_port: ''
+
+# Tells whether the "synapse" scraper configuration is enabled.
+matrix_prometheus_scraper_synapse_enabled: false
+
+# Tells whether to download and load a Synapse rules file
+matrix_prometheus_scraper_synapse_rules_enabled: "{{ matrix_prometheus_scraper_synapse_enabled }}"
+matrix_prometheus_scraper_synapse_rules_synapse_tag: "master"
+matrix_prometheus_scraper_synapse_rules_download_url: "https://raw.githubusercontent.com/matrix-org/synapse/{{ matrix_prometheus_scraper_synapse_rules_synapse_tag }}/contrib/prometheus/synapse-v2.rules"
+
+matrix_prometheus_scraper_synapse_targets: []
+matrix_prometheus_scraper_synapse_workers_enabled_list: []
+
+# Tells whether the "node" scraper configuration is enabled.
+# This configuration aims to scrape the current node (this server).
+matrix_prometheus_scraper_node_enabled: false
+
+# Target addresses for the "node" scraper configuration.
+# Unless you define this as a non-empty list, it gets populated at runtime with the IP address of `matrix-prometheus-node-exporter` and port 9100.
+matrix_prometheus_scraper_node_targets: []
+
+# Default prometheus configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_prometheus_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_prometheus_configuration_yaml: "{{ lookup('template', 'templates/prometheus.yml.j2') }}"
+
+matrix_prometheus_configuration_extension_yaml: |
+  # Your custom YAML configuration goes here.
+  # This configuration extends the default starting configuration (`matrix_prometheus_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_prometheus_configuration_yaml`.
+
+matrix_prometheus_configuration_extension: "{{ matrix_prometheus_configuration_extension_yaml|from_yaml if matrix_prometheus_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_prometheus_configuration_yaml`.
+matrix_prometheus_configuration: "{{ matrix_prometheus_configuration_yaml|from_yaml|combine(matrix_prometheus_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-prometheus/tasks/init.yml b/roles/matrix-prometheus/tasks/init.yml
new file mode 100644
index 000000000..12fae831a
--- /dev/null
+++ b/roles/matrix-prometheus/tasks/init.yml
@@ -0,0 +1,5 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-prometheus.service'] }}"
+  when: matrix_prometheus_enabled|bool
+
+
diff --git a/roles/matrix-prometheus/tasks/main.yml b/roles/matrix-prometheus/tasks/main.yml
new file mode 100644
index 000000000..20f18cc3b
--- /dev/null
+++ b/roles/matrix-prometheus/tasks/main.yml
@@ -0,0 +1,21 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_prometheus_enabled|bool"
+  tags:
+    - setup-all
+    - setup-prometheus
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: "run_setup|bool and matrix_prometheus_enabled|bool"
+  tags:
+    - setup-all
+    - setup-prometheus
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: "run_setup|bool and not matrix_prometheus_enabled|bool"
+  tags:
+    - setup-all
+    - setup-prometheus
diff --git a/roles/matrix-prometheus/tasks/setup_install.yml b/roles/matrix-prometheus/tasks/setup_install.yml
new file mode 100644
index 000000000..15a692797
--- /dev/null
+++ b/roles/matrix-prometheus/tasks/setup_install.yml
@@ -0,0 +1,50 @@
+---
+
+- name: Ensure matrix-prometheus image is pulled
+  docker_image:
+    name: "{{ matrix_prometheus_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_prometheus_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_prometheus_docker_image_force_pull }}"
+
+- name: Ensure Prometheus paths exists
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_prometheus_base_path }}"
+    - "{{ matrix_prometheus_config_path }}"
+    - "{{ matrix_prometheus_data_path }}"
+
+- name: Download synapse-v2.rules
+  get_url:
+    url: "{{ matrix_prometheus_scraper_synapse_rules_download_url }}"
+    dest: "{{ matrix_prometheus_config_path }}/synapse-v2.rules"
+    force: true
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "matrix_prometheus_scraper_synapse_rules_enabled|bool"
+
+- name: Ensure prometheus.yml installed
+  copy:
+    content: "{{ matrix_prometheus_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_prometheus_config_path }}/prometheus.yml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-prometheus.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-prometheus.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-prometheus.service"
+    mode: 0644
+  register: matrix_prometheus_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-prometheus.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_prometheus_systemd_service_result.changed|bool"
diff --git a/roles/matrix-prometheus/tasks/setup_uninstall.yml b/roles/matrix-prometheus/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..dd46a2228
--- /dev/null
+++ b/roles/matrix-prometheus/tasks/setup_uninstall.yml
@@ -0,0 +1,25 @@
+---
+
+- name: Check existence of matrix-prometheus service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus.service"
+  register: matrix_prometheus_service_stat
+
+- name: Ensure matrix-prometheus is stopped
+  service:
+    name: matrix-prometheus
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_prometheus_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-prometheus.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-prometheus.service"
+    state: absent
+  when: "matrix_prometheus_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-prometheus.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_prometheus_service_stat.stat.exists|bool"
diff --git a/roles/matrix-prometheus/tasks/validate_config.yml b/roles/matrix-prometheus/tasks/validate_config.yml
new file mode 100644
index 000000000..9fcfe12b2
--- /dev/null
+++ b/roles/matrix-prometheus/tasks/validate_config.yml
@@ -0,0 +1,7 @@
+---
+
+- name: Fail if Synapse metrics or Prometheus Node Exporter not enabled
+  fail:
+    msg: >
+      You need to enable `matrix_prometheus_scraper_synapse_enabled` and/or `matrix_prometheus_scraper_node_enabled` for Prometheus grab metrics.
+  when: "not matrix_prometheus_scraper_synapse_enabled and not matrix_prometheus_scraper_node_enabled"
diff --git a/roles/matrix-prometheus/templates/prometheus.yml.j2 b/roles/matrix-prometheus/templates/prometheus.yml.j2
new file mode 100644
index 000000000..869b2da8d
--- /dev/null
+++ b/roles/matrix-prometheus/templates/prometheus.yml.j2
@@ -0,0 +1,59 @@
+#jinja2: lstrip_blocks: "True"
+global:
+  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
+  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
+  # scrape_timeout is set to the global default (10s).
+
+# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
+rule_files:
+    {% if matrix_prometheus_scraper_synapse_rules_enabled %}
+    - 'synapse-v2.rules'
+    {% endif %}
+
+# A scrape configuration containing exactly one endpoint to scrape:
+# Here it's Prometheus itself.
+scrape_configs:
+  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
+  - job_name: 'prometheus'
+
+    # Override the global default and scrape targets from this job every 5 seconds.
+    scrape_interval: 5s
+    scrape_timeout: 5s
+
+    # metrics_path defaults to '/metrics'
+    # scheme defaults to 'http'.
+
+    static_configs:
+      - targets: ['localhost:9090']
+
+  {% if matrix_prometheus_scraper_synapse_enabled %}
+  - job_name: 'synapse'
+    metrics_path: '/_synapse/metrics'
+    static_configs:
+    - targets: {{ matrix_prometheus_scraper_synapse_targets|to_json }}
+      labels:
+        instance: {{ matrix_domain }}
+        job: master
+        index: 0
+  {% for worker in matrix_prometheus_scraper_synapse_workers_enabled_list %}
+  {% if worker.metrics_port != 0 %}
+    - targets: ['matrix-synapse-worker-{{ worker.type }}-{{ worker.instanceId }}:{{ worker.metrics_port }}']
+      labels:
+        instance: {{ matrix_domain }}
+        job: {{ worker.type }}
+        index: {{ worker.instanceId }}
+  {% endif %}
+  {% endfor %}
+  {% endif %}
+
+  {% if matrix_prometheus_scraper_node_enabled %}
+  - job_name: node
+    static_configs:
+      - targets: {{ matrix_prometheus_scraper_node_targets|to_json }}
+  {% endif %}
+
+  {% if matrix_prometheus_scraper_postgres_enabled %}
+  - job_name: postgres
+    static_configs:
+      - targets: {{ matrix_prometheus_scraper_postgres_targets|to_json }}
+  {% endif %}
diff --git a/roles/matrix-prometheus/templates/systemd/matrix-prometheus.service.j2 b/roles/matrix-prometheus/templates/systemd/matrix-prometheus.service.j2
new file mode 100644
index 000000000..ad75d664a
--- /dev/null
+++ b/roles/matrix-prometheus/templates/systemd/matrix-prometheus.service.j2
@@ -0,0 +1,43 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-prometheus
+{% for service in matrix_prometheus_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_prometheus_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus 2>/dev/null'
+
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-prometheus \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_prometheus_container_http_host_bind_port %}
+			-p {{ matrix_prometheus_container_http_host_bind_port }}:9090 \
+			{% endif %}
+			-v {{ matrix_prometheus_config_path }}:/etc/prometheus:z \
+			-v {{ matrix_prometheus_data_path }}:/prometheus:z \
+			{% for arg in matrix_prometheus_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_prometheus_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-prometheus 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-prometheus 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-prometheus
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-redis/defaults/main.yml b/roles/matrix-redis/defaults/main.yml
new file mode 100644
index 000000000..409c7926f
--- /dev/null
+++ b/roles/matrix-redis/defaults/main.yml
@@ -0,0 +1,22 @@
+matrix_redis_enabled: true
+
+matrix_redis_connection_password: ""
+
+matrix_redis_base_path: "{{ matrix_base_data_path }}/redis"
+matrix_redis_data_path: "{{ matrix_redis_base_path }}/data"
+
+matrix_redis_version: 6.2.4-alpine
+matrix_redis_docker_image_v6: "{{ matrix_container_global_registry_prefix }}redis:{{ matrix_redis_version }}"
+matrix_redis_docker_image_latest: "{{ matrix_redis_docker_image_v6 }}"
+matrix_redis_docker_image_to_use: '{{ matrix_redis_docker_image_latest }}'
+
+matrix_redis_docker_image_force_pull: "{{ matrix_redis_docker_image_to_use.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_redis_container_extra_arguments: []
+
+# Controls whether the matrix-redis container exposes a port (tcp/6379 in the container)
+# that can be used to access redis from outside the container
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:6379"), or empty string to not expose.
+matrix_redis_container_redis_bind_port: ""
diff --git a/roles/matrix-redis/tasks/init.yml b/roles/matrix-redis/tasks/init.yml
new file mode 100644
index 000000000..490688512
--- /dev/null
+++ b/roles/matrix-redis/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-redis'] }}"
+  when: matrix_redis_enabled|bool
diff --git a/roles/matrix-redis/tasks/main.yml b/roles/matrix-redis/tasks/main.yml
new file mode 100644
index 000000000..595b09f55
--- /dev/null
+++ b/roles/matrix-redis/tasks/main.yml
@@ -0,0 +1,9 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/setup_redis.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-redis
diff --git a/roles/matrix-redis/tasks/setup_redis.yml b/roles/matrix-redis/tasks/setup_redis.yml
new file mode 100644
index 000000000..6f00282b4
--- /dev/null
+++ b/roles/matrix-redis/tasks/setup_redis.yml
@@ -0,0 +1,99 @@
+---
+
+#
+# Tasks related to setting up an internal redis server
+#
+
+- name: Ensure redis Docker image is pulled
+  docker_image:
+    name: "{{ matrix_redis_docker_image_to_use }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_redis_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_redis_docker_image_force_pull }}"
+  when: matrix_redis_enabled|bool
+
+- name: Ensure redis paths exist
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0700
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_username }}"
+  with_items:
+    - "{{ matrix_redis_base_path }}"
+    - "{{ matrix_redis_data_path }}"
+  when: matrix_redis_enabled|bool
+
+# We do this as a separate task, because:
+# - we'd like to do it for the data path only, not for the base path (which contains root-owned environment variable files we'd like to leave as-is)
+# - we need to do it without `mode`, or we risk making certain `.conf` and other files's executable bit to flip to true
+- name: Ensure redis data path ownership is correct
+  file:
+    path: "{{ matrix_redis_data_path }}"
+    state: directory
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_username }}"
+    recurse: yes
+  when: matrix_redis_enabled|bool
+
+- name: Ensure redis environment variables file created
+  template:
+    src: "{{ role_path }}/templates/{{ item }}.j2"
+    dest: "{{ matrix_redis_base_path }}/{{ item }}"
+    mode: 0644
+  with_items:
+    - "redis.conf"
+  when: matrix_redis_enabled|bool
+
+- name: Ensure matrix-redis.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-redis.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-redis.service"
+    mode: 0644
+  register: matrix_redis_systemd_service_result
+  when: matrix_redis_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-redis.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_redis_enabled|bool and matrix_redis_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of the internal redis server (if it was previously enabled)
+#
+
+- name: Check existence of matrix-redis service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-redis.service"
+  register: matrix_redis_service_stat
+  when: "not matrix_redis_enabled|bool"
+
+- name: Ensure matrix-redis is stopped
+  service:
+    name: matrix-redis
+    state: stopped
+    daemon_reload: yes
+  when: "not matrix_redis_enabled|bool and matrix_redis_service_stat.stat.exists"
+
+- name: Ensure matrix-redis.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-redis.service"
+    state: absent
+  when: "not matrix_redis_enabled|bool and matrix_redis_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-redis.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_redis_enabled|bool and matrix_redis_service_stat.stat.exists"
+
+- name: Check existence of matrix-redis local data path
+  stat:
+    path: "{{ matrix_redis_data_path }}"
+  register: matrix_redis_data_path_stat
+  when: "not matrix_redis_enabled|bool"
+
+# We just want to notify the user. Deleting data is too destructive.
+- name: Notify if matrix-redis local data remains
+  debug:
+    msg: "Note: You are not using a local redis instance, but some old data remains from before in `{{ matrix_redis_data_path }}`. Feel free to delete it."
+  when: "not matrix_redis_enabled|bool and matrix_redis_data_path_stat.stat.exists"
diff --git a/roles/matrix-redis/templates/redis.conf.j2 b/roles/matrix-redis/templates/redis.conf.j2
new file mode 100644
index 000000000..343713566
--- /dev/null
+++ b/roles/matrix-redis/templates/redis.conf.j2
@@ -0,0 +1,4 @@
+#jinja2: lstrip_blocks: "True"
+{% if matrix_redis_connection_password %}
+requirepass {{ matrix_redis_connection_password }}
+{% endif %}
diff --git a/roles/matrix-redis/templates/systemd/matrix-redis.service.j2 b/roles/matrix-redis/templates/systemd/matrix-redis.service.j2
new file mode 100644
index 000000000..5f6699f83
--- /dev/null
+++ b/roles/matrix-redis/templates/systemd/matrix-redis.service.j2
@@ -0,0 +1,37 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Redis server
+After=docker.service
+Requires=docker.service
+
+[Service]
+Type=simple
+ExecStartPre=-/usr/bin/docker stop matrix-redis
+ExecStartPre=-/usr/bin/docker rm matrix-redis
+
+ExecStart=/usr/bin/docker run --rm --name matrix-redis \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,noexec,nosuid,size=100m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_redis_container_redis_bind_port %}
+			-p {{ matrix_redis_container_redis_bind_port }}:6379 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_redis_base_path }}/redis.conf,dst=/usr/local/etc/redis/redis.conf,ro \
+			--mount type=bind,src={{ matrix_redis_data_path }},dst=/data \
+			{% for arg in matrix_redis_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_redis_docker_image_to_use }} \
+			redis-server /usr/local/etc/redis/redis.conf
+
+ExecStop=-/usr/bin/docker stop matrix-redis
+ExecStop=-/usr/bin/docker rm matrix-redis
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-redis
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-registration/defaults/main.yml b/roles/matrix-registration/defaults/main.yml
new file mode 100644
index 000000000..e03891b2e
--- /dev/null
+++ b/roles/matrix-registration/defaults/main.yml
@@ -0,0 +1,116 @@
+# matrix-registration is a simple python application to have a token based matrix registration
+# See: https://zeratax.github.io/matrix-registration/
+
+matrix_registration_enabled: true
+
+matrix_registration_container_image_self_build: false
+matrix_registration_container_image_self_build_repo: "https://github.com/ZerataX/matrix-registration"
+matrix_registration_container_image_self_build_branch: "{{ 'master' if matrix_registration_version == 'latest' else matrix_registration_version }}"
+
+matrix_registration_base_path: "{{ matrix_base_data_path }}/matrix-registration"
+matrix_registration_config_path: "{{ matrix_registration_base_path }}/config"
+matrix_registration_data_path: "{{ matrix_registration_base_path }}/data"
+matrix_registration_docker_src_files_path: "{{ matrix_registration_base_path }}/docker-src"
+
+matrix_registration_version: "v0.7.2"
+
+matrix_registration_docker_image: "{{ matrix_registration_docker_image_name_prefix }}zeratax/matrix-registration:{{ matrix_registration_version }}"
+matrix_registration_docker_image_name_prefix: "{{ 'localhost/' if matrix_registration_container_image_self_build else matrix_container_global_registry_prefix }}"
+matrix_registration_docker_image_force_pull: "{{ matrix_registration_docker_image.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_registration_container_extra_arguments: []
+
+# List of systemd services that matrix-registration.service depends on
+matrix_registration_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-registration.service wants
+matrix_registration_systemd_wanted_services_list: []
+
+# Controls whether the matrix-registration container exposes its HTTP port (tcp/5000 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8767"), or empty string to not expose.
+matrix_registration_container_http_host_bind_port: ''
+
+# Database-related configuration fields.
+#
+# To use SQLite, stick to these defaults.
+#
+# To use Postgres:
+# - change the engine (`matrix_registration_database_engine: 'postgres'`)
+# - adjust your database credentials via the `matrix_registration_postgres_*` variables
+matrix_registration_database_engine: 'sqlite'
+
+matrix_registration_sqlite_database_path_local: "{{ matrix_registration_data_path }}/db.sqlite3"
+matrix_registration_sqlite_database_path_in_container: "/data/db.sqlite3"
+
+matrix_registration_database_username: 'matrix_registration'
+matrix_registration_database_password: 'some-password'
+matrix_registration_database_hostname: 'matrix-postgres'
+matrix_registration_database_port: 5432
+matrix_registration_database_name: 'matrix_registration'
+
+matrix_registration_database_connection_string: 'postgresql://{{ matrix_registration_database_username }}:{{ matrix_registration_database_password }}@{{ matrix_registration_database_hostname }}:{{ matrix_registration_database_port }}/{{ matrix_registration_database_name }}'
+
+# For some reason, matrix-registraiton expects the `db` field to be like this: `sqlite:////data/db.sqlite3`.
+# (seems like one too many slashes, but..)
+matrix_registration_db: "{{
+	{
+		'sqlite': ('sqlite:///' + matrix_registration_sqlite_database_path_in_container),
+		'postgres': matrix_registration_database_connection_string,
+	}[matrix_registration_database_engine]
+}}"
+
+
+# The path at which Matrix Registration will be exposed on `matrix.DOMAIN`
+# (only applies when matrix-nginx-proxy is used).
+matrix_registration_public_endpoint: /matrix-registration
+
+matrix_registration_base_url: "{{ matrix_registration_public_endpoint }}"
+
+matrix_registration_api_register_endpoint: "{{ matrix_homeserver_url }}{{ matrix_registration_public_endpoint }}/register"
+matrix_registration_api_token_endpoint: "{{ matrix_homeserver_url }}{{ matrix_registration_public_endpoint }}/token"
+
+matrix_registration_api_validate_certs: true
+
+# The URL to your homeserver (e.g.: `https://matrix.DOMAIN`).
+# A local (in-container address) is preferable.
+matrix_registration_server_location: ""
+
+matrix_registration_server_name: "{{ matrix_domain }}"
+
+# matrix_registration_shared_secret needs to match the homeserver's registration secret.
+# For Synapse, that's the `registration_shared_secret` setting.
+matrix_registration_shared_secret: ""
+
+# matrix_registration_admin_secret is your own admin secret for using matrix-registration (creating new tokens, etc.)
+matrix_registration_admin_secret: ""
+
+matrix_registration_riot_instance: "https://riot.im/app/"
+
+# Default matrix-registration configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_registration_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_registration_configuration_yaml: "{{ lookup('template', 'templates/config.yaml.j2') }}"
+
+matrix_registration_configuration_extension_yaml: |
+  # Your custom YAML configuration for registration goes here.
+  # This configuration extends the default starting configuration (`matrix_registration_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_registration_configuration_yaml`.
+  #
+  # Example configuration extension follows:
+  #
+  # password:
+  #   min_length: 12
+
+matrix_registration_configuration_extension: "{{ matrix_registration_configuration_extension_yaml|from_yaml if matrix_registration_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final matrix-registration configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_registration_configuration_yaml`.
+matrix_registration_configuration: "{{ matrix_registration_configuration_yaml|from_yaml|combine(matrix_registration_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-registration/tasks/generate_token.yml b/roles/matrix-registration/tasks/generate_token.yml
new file mode 100644
index 000000000..ae5bdf4c2
--- /dev/null
+++ b/roles/matrix-registration/tasks/generate_token.yml
@@ -0,0 +1,50 @@
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `one_time` variable needs to be provided to this playbook, via --extra-vars"
+  when: "one_time is not defined or one_time not in ['yes', 'no']"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `ex_date` variable (expiration date) needs to be provided to this playbook, via --extra-vars"
+  when: "ex_date is not defined or ex_date == '<date>'"
+
+- name: Call matrix-registration token creation API
+  uri:
+    url: "{{ matrix_registration_api_token_endpoint }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_registration_api_validate_certs }}"
+    headers:
+      Content-Type: application/json
+      Authorization: "SharedSecret {{ matrix_registration_admin_secret }}"
+    method: POST
+    body_format: json
+    body: |
+      {
+        "one_time": {{ 'true' if one_time == 'yes' else 'false' }},
+        "ex_date": {{ ex_date|to_json }}
+      }
+  check_mode: no
+  register: matrix_registration_api_result
+
+- set_fact:
+    matrix_registration_api_result_message: >-
+      matrix-registration result:
+
+      Direct registration link (with the token prefilled):
+
+      {{ matrix_registration_api_register_endpoint }}?token={{ matrix_registration_api_result.json.name }}
+
+      Full token details are:
+
+      {{ matrix_registration_api_result.json }}
+  check_mode: no
+
+- name: Inject result message into matrix_playbook_runtime_results
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [matrix_registration_api_result_message]
+      }}
+  check_mode: no
diff --git a/roles/matrix-registration/tasks/init.yml b/roles/matrix-registration/tasks/init.yml
new file mode 100644
index 000000000..32a35c7da
--- /dev/null
+++ b/roles/matrix-registration/tasks/init.yml
@@ -0,0 +1,68 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_registration_container_image_self_build and matrix_registration_enabled"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-registration.service'] }}"
+  when: matrix_registration_enabled|bool
+
+- block:
+  - name: Fail if matrix-nginx-proxy role already executed
+    fail:
+      msg: >-
+        Trying to append matrix-registration's reverse-proxying configuration to matrix-nginx-proxy,
+        but it's pointless since the matrix-nginx-proxy role had already executed.
+        To fix this, please change the order of roles in your plabook,
+        so that the matrix-nginx-proxy role would run after the matrix-registration role.
+    when: matrix_nginx_proxy_role_executed|default(False)|bool
+
+  - name: Generate matrix-registration proxying configuration for matrix-nginx-proxy
+    set_fact:
+      matrix_registration_matrix_nginx_proxy_configuration: |
+        rewrite ^{{ matrix_registration_public_endpoint }}$ $scheme://$server_name{{ matrix_registration_public_endpoint }}/ permanent;
+        rewrite ^{{ matrix_registration_public_endpoint }}/$ $scheme://$server_name{{ matrix_registration_public_endpoint }}/register redirect;
+
+        location ~ ^{{ matrix_registration_public_endpoint }}/(.*) {
+        {% if matrix_nginx_proxy_enabled|default(False) %}
+          {# Use the embedded DNS resolver in Docker containers to discover the service #}
+          resolver 127.0.0.11 valid=5s;
+          set $backend "matrix-registration:5000";
+          proxy_pass http://$backend/$1;
+        {% else %}
+          {# Generic configuration for use outside of our container setup #}
+          proxy_pass http://127.0.0.1:8767/$1;
+        {% endif %}
+
+          {#
+            Workaround matrix-registration serving the background image at /static
+            (see https://github.com/ZerataX/matrix-registration/issues/47)
+          #}
+          sub_filter_once off;
+          sub_filter_types text/css;
+          sub_filter "/static/" "{{ matrix_registration_public_endpoint }}/static/";
+        }
+
+  - name: Register matrix-registration proxying configuration with matrix-nginx-proxy
+    set_fact:
+      matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks: |
+        {{
+          matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks|default([])
+          +
+          [matrix_registration_matrix_nginx_proxy_configuration]
+        }}
+  tags:
+    - always
+  when: matrix_registration_enabled|bool
+
+- name: Warn about reverse-proxying if matrix-nginx-proxy not used
+  debug:
+    msg: >-
+      NOTE: You've enabled the matrix-registration tool but are not using the matrix-nginx-proxy
+      reverse proxy.
+      Please make sure that you're proxying the `{{ matrix_registration_public_endpoint }}`
+      URL endpoint to the matrix-registration container.
+      You can expose the container's port using the `matrix_registration_container_http_host_bind_port` variable.
+  when: "matrix_registration_enabled|bool and matrix_nginx_proxy_enabled is not defined"
diff --git a/roles/matrix-registration/tasks/list_tokens.yml b/roles/matrix-registration/tasks/list_tokens.yml
new file mode 100644
index 000000000..dea3eb31f
--- /dev/null
+++ b/roles/matrix-registration/tasks/list_tokens.yml
@@ -0,0 +1,29 @@
+- name: Call matrix-registration list all tokens API
+  uri:
+    url: "{{ matrix_registration_api_token_endpoint }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_registration_api_validate_certs }}"
+    headers:
+      Content-Type: application/json
+      Authorization: "SharedSecret {{ matrix_registration_admin_secret }}"
+    method: GET
+    body_format: json
+  check_mode: no
+  register: matrix_registration_api_result
+
+- set_fact:
+    matrix_registration_api_result_message: >-
+      matrix-registration result:
+
+      {{ matrix_registration_api_result.json | to_nice_json }}
+  check_mode: no
+
+- name: Inject result message into matrix_playbook_runtime_results
+  set_fact:
+    matrix_playbook_runtime_results: |
+      {{
+        matrix_playbook_runtime_results|default([])
+        +
+        [matrix_registration_api_result_message]
+      }}
+  check_mode: no
diff --git a/roles/matrix-registration/tasks/main.yml b/roles/matrix-registration/tasks/main.yml
new file mode 100644
index 000000000..3324e083b
--- /dev/null
+++ b/roles/matrix-registration/tasks/main.yml
@@ -0,0 +1,31 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: "run_setup|bool and matrix_registration_enabled|bool"
+  tags:
+    - setup-all
+    - setup-matrix-registration
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: "run_setup|bool and matrix_registration_enabled|bool"
+  tags:
+    - setup-all
+    - setup-matrix-registration
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: "run_setup|bool and not matrix_registration_enabled|bool"
+  tags:
+    - setup-all
+    - setup-matrix-registration
+
+- import_tasks: "{{ role_path }}/tasks/generate_token.yml"
+  when: "run_setup|bool and matrix_registration_enabled|bool"
+  tags:
+    - generate-matrix-registration-token
+
+- import_tasks: "{{ role_path }}/tasks/list_tokens.yml"
+  when: "run_setup|bool and matrix_registration_enabled|bool"
+  tags:
+    - list-matrix-registration-tokens
diff --git a/roles/matrix-registration/tasks/setup_install.yml b/roles/matrix-registration/tasks/setup_install.yml
new file mode 100644
index 000000000..0d7da9cee
--- /dev/null
+++ b/roles/matrix-registration/tasks/setup_install.yml
@@ -0,0 +1,101 @@
+---
+
+- set_fact:
+    matrix_registration_requires_restart: false
+
+- block:
+    - name: Check if an SQLite database already exists
+      stat:
+        path: "{{ matrix_registration_sqlite_database_path_local }}"
+      register: matrix_registration_sqlite_database_path_local_stat_result
+
+    - block:
+        - set_fact:
+            matrix_postgres_db_migration_request:
+              src: "{{ matrix_registration_sqlite_database_path_local }}"
+              dst: "{{ matrix_registration_database_connection_string }}"
+              caller: "{{ role_path|basename }}"
+              engine_variable_name: 'matrix_registration_database_engine'
+              engine_old: 'sqlite'
+              systemd_services_to_stop: ['matrix-registration.service']
+              # pgloader makes `ex_date` of type `TIMESTAMP WITH TIMEZONE`,
+              # which makes matrix-registration choke on it later on when comparing dates.
+              additional_psql_statements_list:
+                - ALTER TABLE tokens ALTER COLUMN ex_date TYPE TIMESTAMP WITHOUT TIME ZONE;
+              additional_psql_statements_db_name: "{{ matrix_registration_database_name }}"
+
+        - import_tasks: "{{ role_path }}/../matrix-postgres/tasks/util/migrate_db_to_postgres.yml"
+
+        - set_fact:
+            matrix_registration_requires_restart: true
+      when: "matrix_registration_sqlite_database_path_local_stat_result.stat.exists|bool"
+  when: "matrix_registration_database_engine == 'postgres'"
+
+- name: Ensure matrix-registration paths exist
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_registration_base_path }}", when: true }
+    - { path: "{{ matrix_registration_config_path }}", when: true }
+    - { path: "{{ matrix_registration_data_path }}", when: true }
+    - { path: "{{ matrix_registration_docker_src_files_path }}", when: "{{ matrix_registration_container_image_self_build }}"}
+  when: "item.when|bool"
+
+- name: Ensure matrix-registration image is pulled
+  docker_image:
+    name: "{{ matrix_registration_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_registration_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_registration_docker_image_force_pull }}"
+  when: "not matrix_registration_container_image_self_build|bool"
+
+- name: Ensure matrix-registration repository is present when self-building
+  git:
+    repo: "{{ matrix_registration_container_image_self_build_repo }}"
+    dest: "{{ matrix_registration_docker_src_files_path }}"
+    version: "{{ matrix_registration_container_image_self_build_branch }}"
+    force: "yes"
+  register: matrix_registration_git_pull_results
+  when: "matrix_registration_container_image_self_build|bool"
+
+- name: Ensure matrix-registration Docker image is built
+  docker_image:
+    name: "{{ matrix_registration_docker_image }}"
+    source: build
+    force_source: "{{ matrix_registration_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_registration_git_pull_results.changed }}"
+    build:
+      dockerfile: Dockerfile
+      path: "{{ matrix_registration_docker_src_files_path }}"
+      pull: yes
+  when: "matrix_registration_container_image_self_build|bool"
+
+- name: Ensure matrix-registration config installed
+  copy:
+    content: "{{ matrix_registration_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_registration_config_path }}/config.yaml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-registration.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-registration.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-registration.service"
+    mode: 0644
+  register: matrix_registration_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-registration.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_registration_systemd_service_result.changed|bool"
+
+- name: Ensure matrix-registration.service restarted, if necessary
+  service:
+    name: "matrix-registration.service"
+    state: restarted
+  when: "matrix_registration_requires_restart|bool"
diff --git a/roles/matrix-registration/tasks/setup_uninstall.yml b/roles/matrix-registration/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..573f8170b
--- /dev/null
+++ b/roles/matrix-registration/tasks/setup_uninstall.yml
@@ -0,0 +1,30 @@
+---
+
+- name: Check existence of matrix-registration service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-registration.service"
+  register: matrix_registration_service_stat
+
+- name: Ensure matrix-registration is stopped
+  service:
+    name: matrix-registration
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_registration_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-registration.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-registration.service"
+    state: absent
+  when: "matrix_registration_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-registration.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_registration_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-registration Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_registration_docker_image }}"
+    state: absent
diff --git a/roles/matrix-registration/tasks/validate_config.yml b/roles/matrix-registration/tasks/validate_config.yml
new file mode 100644
index 000000000..90466b46c
--- /dev/null
+++ b/roles/matrix-registration/tasks/validate_config.yml
@@ -0,0 +1,20 @@
+---
+
+- name: Fail if required matrix-registration settings not defined
+  fail:
+    msg: >
+      You need to define a required configuration setting (`{{ item }}`) for using matrix-registration.
+  when: "vars[item] == ''"
+  with_items:
+    - "matrix_registration_shared_secret"
+    - "matrix_registration_admin_secret"
+    - "matrix_registration_server_location"
+
+- name: (Deprecation) Catch and report renamed settings
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in vars"
+  with_items:
+    - {'old': 'matrix_registration_docker_repo', 'new': 'matrix_registration_container_image_self_build_repo'}
diff --git a/roles/matrix-registration/templates/config.yaml.j2 b/roles/matrix-registration/templates/config.yaml.j2
new file mode 100644
index 000000000..39211b249
--- /dev/null
+++ b/roles/matrix-registration/templates/config.yaml.j2
@@ -0,0 +1,31 @@
+server_location: {{ matrix_registration_server_location|to_json }}
+server_name: {{ matrix_registration_server_name|to_json }}
+shared_secret: {{ matrix_registration_shared_secret|to_json }}
+admin_secret: {{ matrix_registration_admin_secret|to_json }}
+riot_instance: {{ matrix_registration_riot_instance|to_json }}
+db: {{ matrix_registration_db|to_json }}
+host: '0.0.0.0'
+port: 5000
+rate_limit: ["100 per day", "10 per minute"]
+allow_cors: false
+logging:
+  disable_existing_loggers: False
+  version: 1
+  root:
+    level: DEBUG
+    handlers: [console]
+  formatters:
+    brief:
+      format: '%(name)s - %(levelname)s - %(message)s'
+    precise:
+      format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+  handlers:
+    console:
+      class: logging.StreamHandler
+      level: INFO
+      formatter: brief
+      stream: ext://sys.stdout
+# password requirements
+password:
+  min_length: 8
+base_url: {{ matrix_registration_base_url|to_json }}
diff --git a/roles/matrix-registration/templates/systemd/matrix-registration.service.j2 b/roles/matrix-registration/templates/systemd/matrix-registration.service.j2
new file mode 100644
index 000000000..e73e3e5fc
--- /dev/null
+++ b/roles/matrix-registration/templates/systemd/matrix-registration.service.j2
@@ -0,0 +1,42 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-registration
+{% for service in matrix_registration_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_registration_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-registration 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-registration 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-registration \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_registration_container_http_host_bind_port %}
+			-p {{ matrix_registration_container_http_host_bind_port }}:5000 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_registration_config_path }},dst=/config,ro \
+			--mount type=bind,src={{ matrix_registration_data_path }},dst=/data \
+			{% for arg in matrix_registration_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_registration_docker_image }} \
+			serve
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-registration 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-registration 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-registration
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-sygnal/defaults/main.yml b/roles/matrix-sygnal/defaults/main.yml
new file mode 100644
index 000000000..476ac2ad4
--- /dev/null
+++ b/roles/matrix-sygnal/defaults/main.yml
@@ -0,0 +1,95 @@
+# Sygnal is a reference Push Gateway for Matrix.
+# To make use of it for delivering push notificatins, you'll need to develop/build your own Matrix app.
+# Learn more here: https://github.com/matrix-org/sygnal
+matrix_sygnal_enabled: false
+
+matrix_sygnal_base_path: "{{ matrix_base_data_path }}/sygnal"
+matrix_sygnal_config_path: "{{ matrix_sygnal_base_path }}/config"
+matrix_sygnal_data_path: "{{ matrix_sygnal_base_path }}/data"
+
+matrix_sygnal_version: v0.9.0
+matrix_sygnal_docker_image: "{{ matrix_container_global_registry_prefix }}matrixdotorg/sygnal:{{ matrix_sygnal_version }}"
+matrix_sygnal_docker_image_force_pull: "{{ matrix_sygnal_docker_image.endswith(':latest') }}"
+
+# List of systemd services that matrix-sygnal.service depends on.
+matrix_sygnal_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-sygnal.service wants
+matrix_sygnal_systemd_wanted_services_list: []
+
+# Controls whether the matrix-sygnal container exposes its HTTP port (tcp/6000 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:6000"), or empty string to not expose.
+matrix_sygnal_container_http_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_sygnal_container_extra_arguments: []
+
+# Database-related configuration fields.
+#
+# To use SQLite, stick to these defaults.
+#
+# To use Postgres:
+# - change the engine (`matrix_sygnal_database_engine: 'postgres'`)
+# - adjust your database credentials via the `matrix_sygnal_postgres_*` variables
+matrix_sygnal_database_engine: 'sqlite'
+
+matrix_sygnal_sqlite_database_path_local: "{{ matrix_sygnal_data_path }}/sygnal.db"
+matrix_sygnal_sqlite_database_path_in_container: "/data/sygnal.db"
+
+matrix_sygnal_database_username: 'matrix_sygnal'
+matrix_sygnal_database_password: 'some-password'
+matrix_sygnal_database_hostname: 'matrix-postgres'
+matrix_sygnal_database_port: 5432
+matrix_sygnal_database_name: 'matrix_sygnal'
+
+matrix_sygnal_database_connection_string: 'postgres://{{ matrix_sygnal_database_username }}:{{ matrix_sygnal_database_password }}@{{ matrix_sygnal_database_hostname }}:{{ matrix_sygnal_database_port }}/{{ matrix_sygnal_database_name }}'
+
+# A map (dictionary) of apps instances that this server works with.
+#
+# Example configuration:
+#
+# matrix_sygnal_apps:
+#   com.example.myapp.ios:
+#     type: apns
+#     # .. more configuration ..
+#   com.example.myapp.android:
+#     type: gcm
+#     api_key: your_api_key_for_gcm
+#     # .. more configuration ..
+#
+# The APNS configuration needs to reference some certificate files.
+# One can put these in the `matrix_sygnal_data_path` directory (`/matrix/sygnal/data`), mounted to `/data` in the container.
+# The `matrix_sygnal_apps` paths need to use the in-container path (`/data`).
+# To install these files via the playbook, one can use the `matrix-aux` role.
+# Examples and more details are available in `docs/configuring-playbook-sygnal.md`.
+matrix_sygnal_apps: []
+
+matrix_sygnal_metrics_prometheus_enabled: false
+
+# Default Sygnal configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_sygnal_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_sygnal_configuration_yaml: "{{ lookup('template', 'templates/sygnal.yaml.j2') }}"
+
+matrix_sygnal_configuration_extension_yaml: |
+  # Your custom YAML configuration for Sygnal goes here.
+  # This configuration extends the default starting configuration (`matrix_sygnal_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_sygnal_configuration_yaml`.
+  #
+  # Example configuration extension follows:
+  # metrics:
+  #   opentracing:
+  #     enabled: true
+
+matrix_sygnal_configuration_extension: "{{ matrix_sygnal_configuration_extension_yaml|from_yaml if matrix_sygnal_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final sygnal configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_sygnal_configuration_yaml`.
+matrix_sygnal_configuration: "{{ matrix_sygnal_configuration_yaml|from_yaml|combine(matrix_sygnal_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-sygnal/tasks/init.yml b/roles/matrix-sygnal/tasks/init.yml
new file mode 100644
index 000000000..559a3681d
--- /dev/null
+++ b/roles/matrix-sygnal/tasks/init.yml
@@ -0,0 +1,3 @@
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-sygnal.service'] }}"
+  when: matrix_sygnal_enabled|bool
diff --git a/roles/matrix-sygnal/tasks/main.yml b/roles/matrix-sygnal/tasks/main.yml
new file mode 100644
index 000000000..c00862a4b
--- /dev/null
+++ b/roles/matrix-sygnal/tasks/main.yml
@@ -0,0 +1,21 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-sygnal
+
+- import_tasks: "{{ role_path }}/tasks/setup_install.yml"
+  when: run_setup|bool and matrix_sygnal_enabled|bool
+  tags:
+    - setup-all
+    - setup-sygnal
+
+- import_tasks: "{{ role_path }}/tasks/setup_uninstall.yml"
+  when: run_setup|bool and not matrix_sygnal_enabled|bool
+  tags:
+    - setup-all
+    - setup-sygnal
diff --git a/roles/matrix-sygnal/tasks/setup_install.yml b/roles/matrix-sygnal/tasks/setup_install.yml
new file mode 100644
index 000000000..afac61c48
--- /dev/null
+++ b/roles/matrix-sygnal/tasks/setup_install.yml
@@ -0,0 +1,73 @@
+---
+
+- set_fact:
+    matrix_sygnal_requires_restart: false
+
+- block:
+    - name: Check if an SQLite database already exists
+      stat:
+        path: "{{ matrix_sygnal_sqlite_database_path_local }}"
+      register: matrix_sygnal_sqlite_database_path_local_stat_result
+
+    - block:
+        - set_fact:
+            matrix_postgres_db_migration_request:
+              src: "{{ matrix_sygnal_sqlite_database_path_local }}"
+              dst: "{{ matrix_sygnal_database_connection_string }}"
+              caller: "{{ role_path|basename }}"
+              engine_variable_name: 'matrix_sygnal_database_engine'
+              engine_old: 'sqlite'
+              systemd_services_to_stop: ['matrix-sygnal.service']
+              pgloader_options: ['--with "quote identifiers"']
+
+        - import_tasks: "{{ role_path }}/../matrix-postgres/tasks/util/migrate_db_to_postgres.yml"
+
+        - set_fact:
+            matrix_sygnal_requires_restart: true
+      when: "matrix_sygnal_sqlite_database_path_local_stat_result.stat.exists|bool"
+  when: "matrix_sygnal_database_engine == 'postgres'"
+
+- name: Ensure Sygnal image is pulled
+  docker_image:
+    name: "{{ matrix_sygnal_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_sygnal_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_sygnal_docker_image_force_pull }}"
+
+- name: Ensure Sygnal paths exists
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - "{{ matrix_sygnal_base_path }}"
+    - "{{ matrix_sygnal_config_path }}"
+    - "{{ matrix_sygnal_data_path }}"
+
+- name: Ensure Sygnal config installed
+  copy:
+    content: "{{ matrix_sygnal_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_sygnal_config_path }}/sygnal.yaml"
+    mode: 0640
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure matrix-sygnal.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-sygnal.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-sygnal.service"
+    mode: 0644
+  register: matrix_sygnal_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-sygnal.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_sygnal_systemd_service_result.changed|bool"
+
+- name: Ensure matrix-sygnal.service restarted, if necessary
+  service:
+    name: "matrix-sygnal.service"
+    state: restarted
+  when: "matrix_sygnal_requires_restart|bool"
diff --git a/roles/matrix-sygnal/tasks/setup_uninstall.yml b/roles/matrix-sygnal/tasks/setup_uninstall.yml
new file mode 100644
index 000000000..dc50078ca
--- /dev/null
+++ b/roles/matrix-sygnal/tasks/setup_uninstall.yml
@@ -0,0 +1,35 @@
+---
+
+- name: Check existence of matrix-sygnal service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-sygnal.service"
+  register: matrix_sygnal_service_stat
+
+- name: Ensure matrix-sygnal is stopped
+  service:
+    name: matrix-sygnal
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_sygnal_service_stat.stat.exists|bool"
+
+- name: Ensure matrix-sygnal.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-sygnal.service"
+    state: absent
+  when: "matrix_sygnal_service_stat.stat.exists|bool"
+
+- name: Ensure systemd reloaded after matrix-sygnal.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_sygnal_service_stat.stat.exists|bool"
+
+- name: Ensure Sygnal base directory doesn't exist
+  file:
+    path: "{{ matrix_sygnal_base_path }}"
+    state: absent
+
+- name: Ensure Sygnal Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_sygnal_docker_image }}"
+    state: absent
diff --git a/roles/matrix-sygnal/tasks/validate_config.yml b/roles/matrix-sygnal/tasks/validate_config.yml
new file mode 100644
index 000000000..efd64104a
--- /dev/null
+++ b/roles/matrix-sygnal/tasks/validate_config.yml
@@ -0,0 +1,13 @@
+- name: Fail if no Sygnal apps defined
+  fail:
+    msg: >-
+      Enabling Sygnal requires that you specify at least one app in `matrix_sygnal_apps`
+  when: "matrix_sygnal_enabled and matrix_sygnal_apps|length == 0"
+
+- name: Fail if running on a non-supported architecture
+  fail:
+    msg: >-
+      Sygnal can only be used on the amd64 architecture for now.
+      Only amd64 container images are pushed for the `docker.io/matrixdotorg/sygnal` container image.
+      Either use a different image (by redefining `matrix_sygnal_docker_image`) or consider contributing self-building support to this role.
+  when: "matrix_sygnal_enabled and matrix_architecture != 'amd64' and matrix_sygnal_docker_image.startswith('docker.io/matrixdotorg/sygnal')"
diff --git a/roles/matrix-sygnal/templates/sygnal.yaml.j2 b/roles/matrix-sygnal/templates/sygnal.yaml.j2
new file mode 100644
index 000000000..bb8c521d9
--- /dev/null
+++ b/roles/matrix-sygnal/templates/sygnal.yaml.j2
@@ -0,0 +1,288 @@
+##
+# This is a configuration for Sygnal, the reference Push Gateway for Matrix
+# See: matrix.org
+##
+
+# The 'database' setting defines the database that sygnal uses to store all of
+# its data.
+#
+# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or
+# 'psycopg2' (for PostgreSQL).
+#
+# 'args' gives options which are passed through to the database engine,
+# except for options starting 'cp_', which are used to configure the Twisted
+# connection pool. For a reference to valid arguments, see:
+#   * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
+#   * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
+#   * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__
+#
+#
+# Example SQLite configuration:
+#
+#database:
+#  name: sqlite3
+#  args:
+#    dbfile: /path/to/database.db
+#
+#
+# Example Postgres configuration:
+#
+#database:
+#  name: psycopg2
+#  args:
+#    host: localhost
+#    database: sygnal
+#    user: sygnal
+#    password: pass
+#    cp_min: 1
+#    cp_max: 5
+#
+{% if matrix_sygnal_database_engine == 'sqlite' %}
+database:
+  name: sqlite3
+  args:
+    dbfile: {{ matrix_sygnal_sqlite_database_path_in_container|to_json }}
+{% else %}
+database:
+  name: psycopg2
+  args:
+    host: {{ matrix_sygnal_database_hostname|to_json }}
+    database: {{ matrix_sygnal_database_name|to_json }}
+    user: {{ matrix_sygnal_database_username|to_json }}
+    password: {{ matrix_sygnal_database_password|to_json }}
+    cp_min: 1
+    cp_max: 5
+{% endif %}
+
+## Logging #
+#
+log:
+  # Specify a Python logging 'dictConfig', as described at:
+  #   https://docs.python.org/3.7/library/logging.config.html#logging.config.dictConfig
+  #
+  setup:
+    version: 1
+    formatters:
+      normal:
+        format: "%(asctime)s [%(process)d] %(levelname)-5s %(name)s %(message)s"
+    handlers:
+      # This handler prints to Standard Error
+      #
+      stderr:
+        class: "logging.StreamHandler"
+        formatter: "normal"
+        stream: "ext://sys.stderr"
+
+      # This handler prints to Standard Output.
+      #
+      stdout:
+        class: "logging.StreamHandler"
+        formatter: "normal"
+        stream: "ext://sys.stdout"
+
+      # This handler demonstrates logging to a text file on the filesystem.
+      # You can use logrotate(8) to perform log rotation.
+      #
+      #file:
+      #  class: "logging.handlers.WatchedFileHandler"
+      #  formatter: "normal"
+      #  filename: "./sygnal.log"
+    loggers:
+      # sygnal.access contains the access logging lines.
+      # Comment out this section if you don't want to give access logging
+      # any special treatment.
+      #
+      sygnal.access:
+        propagate: false
+        handlers: ["stdout"]
+        level: "INFO"
+
+      # sygnal contains log lines from Sygnal itself.
+      # You can comment out this section to fall back to the root logger.
+      #
+      sygnal:
+        propagate: false
+        handlers: ["stderr"]
+
+    root:
+      # Specify the handler(s) to send log messages to.
+      handlers: ["stderr"]
+      level: "INFO"
+
+    disable_existing_loggers: false
+
+
+  access:
+    # Specify whether or not to trust the IP address in the `X-Forwarded-For`
+    # header. In general, you want to enable this if and only if you are using a
+    # reverse proxy which is configured to emit it.
+    #
+    x_forwarded_for: true
+
+## HTTP Server (Matrix Push Gateway API) #
+#
+http:
+  # Specify a list of interface addresses to bind to.
+  #
+  # This example listens on the IPv4 loopback device:
+  #bind_addresses: ['127.0.0.1']
+  # This example listens on all IPv4 interfaces:
+  #bind_addresses: ['0.0.0.0']
+  # This example listens on all IPv4 and IPv6 interfaces:
+  #bind_addresses: ['0.0.0.0', '::']
+  bind_addresses: ['::']
+
+  # Specify the port number to listen on.
+  #
+  port: 6000
+
+## Proxying for outgoing connections #
+#
+# Specify the URL of a proxy to use for outgoing traffic
+# (e.g. to Apple & Google) if desired.
+# Currently only HTTP proxies with CONNECT capability are supported.
+#
+# If you do not specify a value, the `HTTPS_PROXY` environment variable will
+# be used if present. Otherwise, no proxy will be used.
+#
+# Default is unspecified.
+#
+#proxy: 'http://user:secret@prox:8080'
+
+## Metrics #
+#
+metrics:
+  ## Prometheus #
+  #
+  prometheus:
+    # Specify whether or not to enable Prometheus.
+    #
+    enabled: false
+
+    # Specify an address for the Prometheus HTTP Server to listen on.
+    #
+    address: '0.0.0.0'
+
+    # Specify a port for the Prometheus HTTP Server to listen on.
+    #
+    port: 8000
+
+  ## OpenTracing #
+  #
+  opentracing:
+    # Specify whether or not to enable OpenTracing.
+    #
+    enabled: false
+
+    # Specify an implementation of OpenTracing to use. Currently only 'jaeger'
+    # is supported.
+    #
+    implementation: jaeger
+
+    # Specify the service name to be reported to the tracer.
+    #
+    service_name: sygnal
+
+    # Specify configuration values to pass to jaeger_client.
+    #
+    jaeger:
+      sampler:
+        type: 'const'
+        param: 1
+#        local_agent:
+#          reporting_host: '127.0.0.1'
+#          reporting_port:
+      logging: true
+
+  ## Sentry #
+  #
+  sentry:
+    # Specify whether or not to enable Sentry.
+    #
+    enabled: false
+
+    # Specify your Sentry DSN if you enable Sentry
+    #
+    #dsn: "https://<key>@sentry.example.org/<project>"
+
+## Pushkins/Apps #
+#
+# Add a section for every push application here.
+# Specify the pushkey for the application and also the type.
+# For the type, you may specify a fully-qualified Python classname if desired.
+#
+#apps:
+  # This is an example APNs push configuration
+  #
+  #com.example.myapp.ios:
+  #  type: apns
+  #
+  #  # Authentication
+  #  #
+  #  # Two methods of authentication to APNs are currently supported.
+  #  #
+  #  # You can authenticate using a key:
+  #  keyfile: my_key.p8
+  #  key_id: MY_KEY_ID
+  #  team_id: MY_TEAM_ID
+  #  topic: MY_TOPIC
+  #
+  #  # Or, a certificate can be used instead:
+  #  certfile: com.example.myApp_prod_APNS.pem
+  #
+  #  # This is the maximum number of in-flight requests *for this pushkin*
+  #  # before additional notifications will be failed.
+  #  # (This is a robustness measure to prevent one pushkin stacking up with
+  #  #  queued requests and saturating the inbound connection queue of a load
+  #  #  balancer or reverse proxy).
+  #  # Defaults to 512 if unset.
+  #  #
+  #  #inflight_request_limit: 512
+  #
+  #  # Specifies whether to use the production or sandbox APNs server. Note that
+  #  # sandbox tokens should only be used with the sandbox server and vice versa.
+  #  #
+  #  # Valid options are:
+  #  #   * production
+  #  #   * sandbox
+  #  #
+  #  # The default is 'production'. Uncomment to use the sandbox instance.
+  #  #platform: sandbox
+
+  # This is an example GCM/FCM push configuration.
+  #
+  #com.example.myapp.android:
+  #  type: gcm
+  #  api_key: your_api_key_for_gcm
+  #
+  #  # This is the maximum number of connections to GCM servers at any one time
+  #  # the default is 20.
+  #  #max_connections: 20
+  #
+  #  # This is the maximum number of in-flight requests *for this pushkin*
+  #  # before additional notifications will be failed.
+  #  # (This is a robustness measure to prevent one pushkin stacking up with
+  #  #  queued requests and saturating the inbound connection queue of a load
+  #  #  balancer or reverse proxy).
+  #  # Defaults to 512 if unset.
+  #  #
+  #  #inflight_request_limit: 512
+  #
+  #  # This allows you to specify additional options to send to Firebase.
+  #  #
+  #  # Of particular interest, admins who wish to support iOS apps using Firebase
+  #  # probably wish to set content_available, and may need to set mutable_content.
+  #  # (content_available allows your iOS app to be woken up by data messages,
+  #  # and mutable_content allows your notification to be modified by a
+  #  # Notification Service app extension).
+  #  #
+  #  # See https://firebase.google.com/docs/cloud-messaging/http-server-ref
+  #  # for the exhaustive list of valid options.
+  #  #
+  #  # Do not specify `data`, `priority`, `to` or `registration_ids` as they may
+  #  # be overwritten or lead to an invalid request.
+  #  #
+  #  #fcm_options:
+  #  #  content_available: true
+  #  #  mutable_content: true
+apps: {{ matrix_sygnal_apps|to_json }}
diff --git a/roles/matrix-sygnal/templates/systemd/matrix-sygnal.service.j2 b/roles/matrix-sygnal/templates/systemd/matrix-sygnal.service.j2
new file mode 100644
index 000000000..019ab40c0
--- /dev/null
+++ b/roles/matrix-sygnal/templates/systemd/matrix-sygnal.service.j2
@@ -0,0 +1,42 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Sygnal
+{% for service in matrix_sygnal_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_sygnal_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-sygnal 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-sygnal 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-sygnal \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--env=SYGNAL_CONF=/config/sygnal.yaml \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_sygnal_container_http_host_bind_port %}
+			-p {{ matrix_sygnal_container_http_host_bind_port }}:6000 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_sygnal_config_path }},dst=/config \
+			--mount type=bind,src={{ matrix_sygnal_data_path }},dst=/data \
+			{% for arg in matrix_sygnal_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_sygnal_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-sygnal 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-sygnal 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-sygnal
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-synapse-admin/defaults/main.yml b/roles/matrix-synapse-admin/defaults/main.yml
new file mode 100644
index 000000000..069b62794
--- /dev/null
+++ b/roles/matrix-synapse-admin/defaults/main.yml
@@ -0,0 +1,32 @@
+# matrix-synapse-admin is a web UI for mananging the Synapse Matrix server
+# See: https://github.com/Awesome-Technologies/synapse-admin
+
+matrix_synapse_admin_enabled: true
+
+matrix_synapse_admin_container_self_build: false
+matrix_synapse_admin_container_self_build_repo: "https://github.com/Awesome-Technologies/synapse-admin.git"
+
+matrix_synapse_admin_docker_src_files_path: "{{ matrix_base_data_path }}/synapse-admin/docker-src"
+
+matrix_synapse_admin_version: 0.8.1
+matrix_synapse_admin_docker_image: "{{ matrix_synapse_admin_docker_image_name_prefix }}awesometechnologies/synapse-admin:{{ matrix_synapse_admin_version }}"
+matrix_synapse_admin_docker_image_name_prefix: "{{ 'localhost/' if matrix_synapse_admin_container_self_build else matrix_container_global_registry_prefix }}"
+matrix_synapse_admin_docker_image_force_pull: "{{ matrix_synapse_admin_docker_image.endswith(':latest') }}"
+
+# A list of extra arguments to pass to the container
+matrix_synapse_admin_container_extra_arguments: []
+
+# List of systemd services that matrix-synapse-admin.service depends on
+matrix_synapse_admin_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-synapse-admin.service wants
+matrix_synapse_admin_systemd_wanted_services_list: []
+
+# Controls whether the matrix-synapse-admin container exposes its HTTP port (tcp/80 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8766"), or empty string to not expose.
+matrix_synapse_admin_container_http_host_bind_port: ''
+
+# The path at which Synapse Admin will be exposed on `matrix.DOMAIN`
+# (only applies when matrix-nginx-proxy is used).
+matrix_synapse_admin_public_endpoint: /synapse-admin
diff --git a/roles/matrix-synapse-admin/tasks/init.yml b/roles/matrix-synapse-admin/tasks/init.yml
new file mode 100644
index 000000000..9e1710156
--- /dev/null
+++ b/roles/matrix-synapse-admin/tasks/init.yml
@@ -0,0 +1,59 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_synapse_admin_container_self_build and matrix_synapse_admin_enabled"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-synapse-admin.service'] }}"
+  when: matrix_synapse_admin_enabled|bool
+
+- block:
+  - name: Fail if matrix-nginx-proxy role already executed
+    fail:
+      msg: >-
+        Trying to append Synapse Admin's reverse-proxying configuration to matrix-nginx-proxy,
+        but it's pointless since the matrix-nginx-proxy role had already executed.
+        To fix this, please change the order of roles in your plabook,
+        so that the matrix-nginx-proxy role would run after the matrix-synapse-admin role.
+    when: matrix_nginx_proxy_role_executed|default(False)|bool
+
+  - name: Generate Synapse Admin proxying configuration for matrix-nginx-proxy
+    set_fact:
+      matrix_synapse_admin_matrix_nginx_proxy_configuration: |
+        rewrite ^{{ matrix_synapse_admin_public_endpoint }}$ $scheme://$server_name{{ matrix_synapse_admin_public_endpoint }}/ permanent;
+
+        location ~ ^{{ matrix_synapse_admin_public_endpoint }}/(.*) {
+        {% if matrix_nginx_proxy_enabled|default(False) %}
+          {# Use the embedded DNS resolver in Docker containers to discover the service #}
+          resolver 127.0.0.11 valid=5s;
+          set $backend "matrix-synapse-admin:80";
+          proxy_pass http://$backend/$1;
+        {% else %}
+          {# Generic configuration for use outside of our container setup #}
+          proxy_pass http://127.0.0.1:8766/$1;
+        {% endif %}
+        }
+
+  - name: Register Synapse Admin proxying configuration with matrix-nginx-proxy
+    set_fact:
+      matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks: |
+        {{
+          matrix_nginx_proxy_proxy_matrix_additional_server_configuration_blocks|default([])
+          +
+          [matrix_synapse_admin_matrix_nginx_proxy_configuration]
+        }}
+  tags:
+    - always
+  when: matrix_synapse_admin_enabled|bool
+
+- name: Warn about reverse-proxying if matrix-nginx-proxy not used
+  debug:
+    msg: >-
+      NOTE: You've enabled the Synapse Admin tool but are not using the matrix-nginx-proxy
+      reverse proxy.
+      Please make sure that you're proxying the `{{ matrix_synapse_admin_public_endpoint }}`
+      URL endpoint to the matrix-synapse-admin container.
+      You can expose the container's port using the `matrix_synapse_admin_container_http_host_bind_port` variable.
+  when: "matrix_synapse_admin_enabled|bool and matrix_nginx_proxy_enabled is not defined"
diff --git a/roles/matrix-synapse-admin/tasks/main.yml b/roles/matrix-synapse-admin/tasks/main.yml
new file mode 100644
index 000000000..b5cb16893
--- /dev/null
+++ b/roles/matrix-synapse-admin/tasks/main.yml
@@ -0,0 +1,14 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-synapse-admin
+
+- import_tasks: "{{ role_path }}/tasks/setup.yml"
+  tags:
+    - setup-all
+    - setup-synapse-admin
diff --git a/roles/matrix-synapse-admin/tasks/setup.yml b/roles/matrix-synapse-admin/tasks/setup.yml
new file mode 100644
index 000000000..002ff68d1
--- /dev/null
+++ b/roles/matrix-synapse-admin/tasks/setup.yml
@@ -0,0 +1,80 @@
+---
+
+#
+# Tasks related to setting up matrix-synapse-admin
+#
+
+- name: Ensure matrix-synapse-admin image is pulled
+  docker_image:
+    name: "{{ matrix_synapse_admin_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_synapse_admin_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_synapse_admin_docker_image_force_pull }}"
+  when: "matrix_synapse_admin_enabled|bool and not matrix_synapse_admin_container_self_build|bool"
+
+- name: Ensure matrix-synapse-admin repository is present when self-building
+  git:
+    repo: "{{ matrix_synapse_admin_container_self_build_repo }}"
+    dest: "{{ matrix_synapse_admin_docker_src_files_path }}"
+    force: "yes"
+  register: matrix_synapse_admin_git_pull_results
+  when: "matrix_synapse_admin_enabled|bool and matrix_synapse_admin_container_self_build|bool"
+
+- name: Ensure matrix-synapse-admin Docker image is built
+  docker_image:
+    name: "{{ matrix_synapse_admin_docker_image }}"
+    source: build
+    force_source: "{{ matrix_synapse_admin_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_synapse_admin_git_pull_results.changed }}"
+    build:
+      dockerfile: Dockerfile
+      path: "{{ matrix_synapse_admin_docker_src_files_path }}"
+      pull: yes
+  when: "matrix_synapse_admin_enabled|bool and matrix_synapse_admin_container_self_build|bool"
+
+- name: Ensure matrix-synapse-admin.service installed
+  template:
+    src: "{{ role_path }}/templates/systemd/matrix-synapse-admin.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-synapse-admin.service"
+    mode: 0644
+  register: matrix_synapse_admin_systemd_service_result
+  when: matrix_synapse_admin_enabled|bool
+
+- name: Ensure systemd reloaded after matrix-synapse-admin.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_synapse_admin_enabled|bool and matrix_synapse_admin_systemd_service_result.changed"
+
+#
+# Tasks related to getting rid of matrix-synapse-admin (if it was previously enabled)
+#
+
+- name: Check existence of matrix-synapse-admin service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-synapse-admin.service"
+  register: matrix_synapse_admin_service_stat
+
+- name: Ensure matrix-synapse-admin is stopped
+  service:
+    name: matrix-synapse-admin
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "not matrix_synapse_admin_enabled|bool and matrix_synapse_admin_service_stat.stat.exists"
+
+- name: Ensure matrix-synapse-admin.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-synapse-admin.service"
+    state: absent
+  when: "not matrix_synapse_admin_enabled|bool and matrix_synapse_admin_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-synapse-admin.service removal
+  service:
+    daemon_reload: yes
+  when: "not matrix_synapse_admin_enabled|bool and matrix_synapse_admin_service_stat.stat.exists"
+
+- name: Ensure matrix-synapse-admin Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_synapse_admin_docker_image }}"
+    state: absent
+  when: "not matrix_synapse_admin_enabled|bool"
diff --git a/roles/matrix-synapse-admin/tasks/validate_config.yml b/roles/matrix-synapse-admin/tasks/validate_config.yml
new file mode 100644
index 000000000..e08680e03
--- /dev/null
+++ b/roles/matrix-synapse-admin/tasks/validate_config.yml
@@ -0,0 +1,10 @@
+---
+
+- name: (Deprecation) Catch and report renamed settings
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in vars"
+  with_items:
+    - {'old': 'matrix_synapse_admin_docker_repo', 'new': 'matrix_synapse_admin_container_self_build_repo'}
diff --git a/roles/matrix-synapse-admin/templates/systemd/matrix-synapse-admin.service.j2 b/roles/matrix-synapse-admin/templates/systemd/matrix-synapse-admin.service.j2
new file mode 100644
index 000000000..4823d89c3
--- /dev/null
+++ b/roles/matrix-synapse-admin/templates/systemd/matrix-synapse-admin.service.j2
@@ -0,0 +1,42 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=matrix-synapse-admin
+{% for service in matrix_synapse_admin_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+{% endfor %}
+{% for service in matrix_synapse_admin_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-synapse-admin 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-synapse-admin 2>/dev/null'
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-synapse-admin \
+			--log-driver=none \
+			--cap-drop=ALL \
+			--cap-add=CHOWN \
+			--cap-add=NET_BIND_SERVICE \
+			--cap-add=SETUID \
+			--cap-add=SETGID \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_synapse_admin_container_http_host_bind_port %}
+			-p {{ matrix_synapse_admin_container_http_host_bind_port }}:80 \
+			{% endif %}
+			{% for arg in matrix_synapse_admin_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_synapse_admin_docker_image }}
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-synapse-admin 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-synapse-admin 2>/dev/null'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-synapse-admin
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-synapse/defaults/main.yml b/roles/matrix-synapse/defaults/main.yml
new file mode 100644
index 000000000..02b8d1573
--- /dev/null
+++ b/roles/matrix-synapse/defaults/main.yml
@@ -0,0 +1,612 @@
+# Synapse is a Matrix homeserver
+# See: https://github.com/matrix-org/synapse
+
+matrix_synapse_enabled: true
+
+matrix_synapse_container_image_self_build: false
+matrix_synapse_container_image_self_build_repo: "https://github.com/matrix-org/synapse.git"
+
+matrix_synapse_docker_image: "{{ matrix_synapse_docker_image_name_prefix }}matrixdotorg/synapse:{{ matrix_synapse_docker_image_tag }}"
+matrix_synapse_docker_image_name_prefix: "{{ 'localhost/' if matrix_synapse_container_image_self_build else matrix_container_global_registry_prefix }}"
+# The if statement below may look silly at times (leading to the same version being returned),
+# but ARM-compatible container images are only released 1-7 hours after a release,
+# so we may often be on different versions for different architectures when new Synapse releases come out.
+#
+# amd64 gets released first.
+# arm32 relies on self-building, so the same version can be built immediately.
+# arm64 users need to wait for a prebuilt image to become available.
+matrix_synapse_version: v1.38.0
+matrix_synapse_version_arm64: v1.38.0
+matrix_synapse_docker_image_tag: "{{ matrix_synapse_version if matrix_architecture in ['arm32', 'amd64'] else matrix_synapse_version_arm64 }}"
+matrix_synapse_docker_image_force_pull: "{{ matrix_synapse_docker_image.endswith(':latest') }}"
+
+matrix_synapse_base_path: "{{ matrix_base_data_path }}/synapse"
+matrix_synapse_docker_src_files_path: "{{ matrix_synapse_base_path }}/docker-src"
+matrix_synapse_config_dir_path: "{{ matrix_synapse_base_path }}/config"
+matrix_synapse_storage_path: "{{ matrix_synapse_base_path }}/storage"
+matrix_synapse_media_store_path: "{{ matrix_synapse_storage_path }}/media-store"
+matrix_synapse_ext_path: "{{ matrix_synapse_base_path }}/ext"
+
+# Controls whether the matrix-synapse container exposes the Client/Server API port (tcp/8008 in the container).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8008"), or empty string to not expose.
+matrix_synapse_container_client_api_host_bind_port: ''
+
+# Controls whether the matrix-synapse container exposes the plain (unencrypted) Server/Server (Federation) API port (tcp/8048 in the container).
+#
+# Takes effect only if federation is enabled (matrix_synapse_federation_enabled).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:8048"), or empty string to not expose.
+matrix_synapse_container_federation_api_plain_host_bind_port: ''
+
+# Controls whether the matrix-synapse container exposes the tls (encrypted) Server/Server (Federation) API port (tcp/8448 in the container).
+#
+# Takes effect only if federation is enabled (matrix_synapse_federation_enabled)
+# and TLS support is enabled (matrix_synapse_tls_federation_listener_enabled).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "8448"), or empty string to not expose.
+matrix_synapse_container_federation_api_tls_host_bind_port: ''
+
+# Controls whether the matrix-synapse container exposes the metrics port (tcp/9100 in the container).
+#
+# Takes effect only if metrics are enabled (matrix_synapse_metrics_enabled).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:9100"), or empty string to not expose.
+matrix_synapse_container_metrics_api_host_bind_port: ''
+
+# Controls whether the matrix-synapse container exposes the manhole port (tcp/9000 in the container).
+#
+# Takes effect only if the manhole is enabled (matrix_synapse_manhole_enabled).
+#
+# Takes an "<ip>:<port>" or "<port>" value (e.g. "127.0.0.1:9100"), or empty string to not expose.
+matrix_synapse_container_manhole_api_host_bind_port: ''
+
+# A list of extra arguments to pass to the container
+matrix_synapse_container_extra_arguments: []
+
+# List of systemd services that matrix-synapse.service depends on
+matrix_synapse_systemd_required_services_list: ['docker.service']
+
+# List of systemd services that matrix-synapse.service wants
+matrix_synapse_systemd_wanted_services_list: []
+
+matrix_synapse_in_container_python_packages_path: "/usr/local/lib/python3.8/site-packages"
+
+# Specifies which template files to use when configuring Synapse.
+# If you'd like to have your own different configuration, feel free to copy and paste
+# the original files into your inventory (e.g. in `inventory/host_vars/<host>/`)
+# and then change the specific host's `vars.yaml` file like this:
+# matrix_synapse_template_synapse_homeserver: "{{ playbook_dir }}/inventory/host_vars/<host>/homeserver.yaml.j2"
+matrix_synapse_template_synapse_homeserver: "{{ role_path }}/templates/synapse/homeserver.yaml.j2"
+matrix_synapse_template_synapse_log: "{{ role_path }}/templates/synapse/synapse.log.config.j2"
+
+matrix_synapse_macaroon_secret_key: ""
+matrix_synapse_registration_shared_secret: "{{ matrix_synapse_macaroon_secret_key }}"
+matrix_synapse_allow_guest_access: false
+matrix_synapse_form_secret: "{{ matrix_synapse_macaroon_secret_key }}"
+
+matrix_synapse_max_upload_size_mb: 50
+
+# The tmpfs at /tmp needs to be large enough to handle multiple concurrent file uploads.
+matrix_synapse_tmp_directory_size_mb: "{{ matrix_synapse_max_upload_size_mb * 50 }}"
+
+# Log levels
+# Possible options are defined here https://docs.python.org/3/library/logging.html#logging-levels
+# warning: setting log level to DEBUG will make synapse log sensitive information such
+# as access tokens.
+#
+# Increasing verbosity may lead to an excessive amount of log messages being generated,
+# some of which may get dropped by systemd-journald on certain distributions (like CentOS 7).
+# You can work around it by adding `RateLimitInterval=0` and `RateLimitBurst=0` under `[Storage]` in
+# `/etc/systemd/journald.conf` and restarting the logging service (`systemctl restart systemd-journald`).
+matrix_synapse_log_level: "WARNING"
+matrix_synapse_storage_sql_log_level: "WARNING"
+matrix_synapse_root_log_level: "WARNING"
+
+# Rate limits
+matrix_synapse_rc_message:
+  per_second: 0.2
+  burst_count: 10
+
+matrix_synapse_rc_registration:
+  per_second: 0.17
+  burst_count: 3
+
+matrix_synapse_rc_login:
+  address:
+    per_second: 0.17
+    burst_count: 3
+  account:
+    per_second: 0.17
+    burst_count: 3
+  failed_attempts:
+    per_second: 0.17
+    burst_count: 3
+
+matrix_synapse_rc_admin_redaction:
+  per_second: 1
+  burst_count: 50
+
+matrix_synapse_rc_joins:
+  local:
+    per_second: 0.1
+    burst_count: 3
+  remote:
+    per_second: 0.01
+    burst_count: 3
+
+matrix_synapse_rc_federation:
+  window_size: 1000
+  sleep_limit: 10
+  sleep_delay: 500
+  reject_limit: 50
+  concurrent: 3
+
+matrix_synapse_federation_rr_transactions_per_room_per_second: 50
+
+# Controls whether the TLS federation listener is enabled (tcp/8448).
+# Only makes sense if federation is enabled (`matrix_synapse_federation_enabled`).
+# Note that federation may potentially be enabled as non-TLS on tcp/8048 as well.
+# If you're serving Synapse behind an HTTPS-capable reverse-proxy,
+# you can disable the TLS listener (`matrix_synapse_tls_federation_listener_enabled: false`).
+matrix_synapse_tls_federation_listener_enabled: true
+matrix_synapse_tls_certificate_path: "/data/{{ matrix_server_fqn_matrix }}.tls.crt"
+matrix_synapse_tls_private_key_path: "/data/{{ matrix_server_fqn_matrix }}.tls.key"
+
+# Resource names used by the unsecure HTTP listener. Here only the Client API
+# is defined, see the homeserver config for a full list of valid resource
+# names.
+matrix_synapse_http_listener_resource_names: ["client"]
+
+# Resources served on Synapse's federation port.
+# When disabling federation, we may wish to serve the `openid` resource here,
+# so that services like Dimension and ma1sd can work.
+matrix_synapse_federation_listener_resource_names: "{{ ['federation'] if matrix_synapse_federation_enabled else (['openid'] if matrix_synapse_federation_port_openid_resource_required else []) }}"
+
+# Enable this to allow Synapse to report utilization statistics about your server to matrix.org
+# (things like number of users, number of messages sent, uptime, load, etc.)
+matrix_synapse_report_stats: false
+
+# Controls whether the Matrix server will track presence status (online, offline, unavailable) for users.
+# If users participate in large rooms with many other servers,
+# disabling this will decrease server load significantly.
+matrix_synapse_presence_enabled: true
+
+# Controls whether accessing the server's public rooms directory can be done without authentication.
+# For private servers, you most likely wish to require authentication,
+# unless you know what list of rooms you're publishing to the world and explicitly want to do it.
+matrix_synapse_allow_public_rooms_without_auth: false
+
+# Controls whether remote servers can fetch this server's public rooms directory via federation.
+# For private servers, you most likely wish to forbid it.
+matrix_synapse_allow_public_rooms_over_federation: false
+
+# Whether to require authentication to retrieve profile data (avatars,
+# display names) of other users through the client API. Defaults to
+# 'false'. Note that profile data is also available via the federation
+# API, so this setting is of limited value if federation is enabled on
+# the server.
+matrix_synapse_require_auth_for_profile_requests: false
+
+# Set to true to require a user to share a room with another user in order
+# to retrieve their profile information. Only checked on Client-Server
+# requests. Profile requests from other servers should be checked by the
+# requesting server. Defaults to 'false'.
+matrix_synapse_limit_profile_requests_to_users_who_share_rooms: false
+
+# Set to false to prevent a user's profile data from being retrieved and
+# displayed in a room until they have joined it. By default, a user's
+# profile data is included in an invite event, regardless of the values
+# of the above two settings, and whether or not the users share a server.
+# Defaults to 'true'.
+matrix_synapse_include_profile_data_on_invite: true
+
+# Controls whether people with access to the homeserver can register by themselves.
+matrix_synapse_enable_registration: false
+
+# reCAPTCHA API for validating registration attempts
+matrix_synapse_enable_registration_captcha: false
+matrix_synapse_recaptcha_public_key: ''
+matrix_synapse_recaptcha_private_key: ''
+
+# Allows non-server-admin users to create groups on this server
+matrix_synapse_enable_group_creation: false
+
+# A list of 3PID types which users must supply when registering (possible values: email, msisdn).
+matrix_synapse_registrations_require_3pid: []
+
+# A list of patterns 3pids must match in order to permit registration, e.g.:
+#  - medium: email
+#    pattern: '.*@example\.com'
+#  - medium: msisdn
+#    pattern: '\+44'
+matrix_synapse_allowed_local_3pids: []
+
+# The server to use for email threepid validation. When empty, Synapse does it by itself.
+# Otherwise, this should be pointed to an identity server.
+matrix_synapse_account_threepid_delegates_email: ''
+
+# The server to use for phone number threepid validation. When empty, validation cannot happen, as Synapse doesn't support it.
+# To make it work, this should be pointed to an identity server.
+matrix_synapse_account_threepid_delegates_msisdn: ''
+
+# Users who register on this homeserver will automatically be joined to these rooms.
+# Rooms are to be specified using addresses (e.g. `#address:example.com`)
+matrix_synapse_auto_join_rooms: []
+
+# Controls whether auto-join rooms (`matrix_synapse_auto_join_rooms`) are to be created
+# automatically if they don't already exist.
+matrix_synapse_autocreate_auto_join_rooms: true
+
+# Controls password-peppering for Synapse. Not to be changed after initial setup.
+matrix_synapse_password_config_pepper: ""
+
+# Controls if Synapse allows people to authenticate against its local database.
+# It may be useful to disable this if you've configured additional password providers
+# and only wish authentication to happen through them.
+matrix_synapse_password_config_localdb_enabled: true
+
+# Controls the number of events that Synapse caches in memory.
+matrix_synapse_event_cache_size: "100K"
+
+# Controls cache sizes for Synapse.
+# Raise this to increase cache sizes or lower it to potentially lower memory use.
+# To learn more, see:
+# - https://github.com/matrix-org/synapse#help-synapse-eats-all-my-ram
+# - https://github.com/matrix-org/synapse/issues/3939
+matrix_synapse_caches_global_factor: 0.5
+
+# Controls whether Synapse will federate at all.
+# Disable this to completely isolate your server from the rest of the Matrix network.
+#
+# Disabling this still keeps the federation port exposed, because it may be used for other services (`openid`).
+#
+# Also see:
+# - `matrix_synapse_tls_federation_listener_enabled` if you wish to keep federation enabled,
+# but want to stop the TLS listener (port 8448).
+# - `matrix_synapse_federation_port_enabled` to avoid exposing the federation ports
+matrix_synapse_federation_enabled: true
+
+# Controls whether the federation ports are used at all.
+# One may wish to disable federation (`matrix_synapse_federation_enabled: true`),
+# but still run other resources (like `openid`) on the federation port
+# by enabling them in `matrix_synapse_federation_listener_resource_names`.
+matrix_synapse_federation_port_enabled: "{{ matrix_synapse_federation_enabled or matrix_synapse_federation_port_openid_resource_required }}"
+
+# Controls whether an `openid` listener is to be enabled. Useful when disabling federation,
+# but needing the `openid` APIs for Dimension or an identity server like ma1sd.
+matrix_synapse_federation_port_openid_resource_required: false
+
+# A list of domain names that are allowed to federate with the given Synapse server.
+# An empty list value (`[]`) will also effectively stop federation, but if that's the desired
+# result, it's better to accomplish it by changing `matrix_synapse_federation_enabled`.
+matrix_synapse_federation_domain_whitelist: ~
+
+# A list of additional "volumes" to mount in the container.
+# This list gets populated dynamically based on Synapse extensions that have been enabled.
+# Contains definition objects like this: `{"src": "/outside", "dst": "/inside", "options": "rw|ro|slave|.."}
+#
+# Note: internally, this uses the `-v` flag for mounting the specified volumes.
+# It's better (safer) to use the `--mount` flag for mounting volumes.
+# To use `--mount`, specify it in `matrix_synapse_container_extra_arguments`.
+# Example: `matrix_synapse_container_extra_arguments: ['--mount type=bind,src=/outside,dst=/inside,ro']
+matrix_synapse_container_additional_volumes: []
+
+# A list of additional loggers to register in synapse.log.config.
+# This list gets populated dynamically based on Synapse extensions that have been enabled.
+# Contains definition objects like this: `{"name": "..", "level": "DEBUG"}
+matrix_synapse_additional_loggers: []
+
+# A list of appservice config files (in-container filesystem paths).
+# This list gets populated dynamically based on Synapse extensions that have been enabled.
+# You may wish to use this together with `matrix_synapse_container_additional_volumes` or `matrix_synapse_container_extra_arguments`.
+matrix_synapse_app_service_config_files: []
+
+# This is set dynamically during execution depending on whether
+# any password providers have been enabled or not.
+matrix_synapse_password_providers_enabled: false
+
+# Whether clients can request to include message content in push notifications
+# sent through third party servers. Setting this to false requires mobile clients
+# to load message content directly from the homeserver.
+matrix_synapse_push_include_content: true
+
+# If url previews should be generated. This will cause a request from Synapse to
+# URLs shared by users.
+matrix_synapse_url_preview_enabled: true
+
+# Enable exposure of metrics to Prometheus
+# See https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md
+matrix_synapse_metrics_enabled: false
+matrix_synapse_metrics_port: 9100
+
+# Enable the Synapse manhole
+# See https://github.com/matrix-org/synapse/blob/master/docs/manhole.md
+matrix_synapse_manhole_enabled: false
+
+# Enable support for Synapse workers
+matrix_synapse_workers_enabled: false
+
+# Specifies worker configuration that should be used when workers are enabled.
+#
+# The posible values (as seen in `matrix_synapse_workers_presets`) are:
+# - "little-federation-helper" - a very minimal worker configuration to improve federation performance
+# - "one-of-each" - one worker of each supported type
+#
+# You can override `matrix_synapse_workers_presets` to define your own presets, which is ill-advised, because it's fragile.
+# To use a more custom configuration, start with one of these presets as a base and configure `matrix_synapse_workers_*_count` variables manually, to suit your liking.
+matrix_synapse_workers_preset: one-of-each
+
+matrix_synapse_workers_presets:
+  little-federation-helper:
+    generic_workers_count: 0
+    pusher_workers_count: 0
+    appservice_workers_count: 0
+    federation_sender_workers_count: 1
+    media_repository_workers_count: 0
+    user_dir_workers_count: 0
+    frontend_proxy_workers_count: 0
+  one-of-each:
+    generic_workers_count: 1
+    pusher_workers_count: 1
+    appservice_workers_count: 1
+    federation_sender_workers_count: 1
+    media_repository_workers_count: 1
+    # Disabled until https://github.com/matrix-org/synapse/issues/8787 is resolved.
+    user_dir_workers_count: 0
+    frontend_proxy_workers_count: 1
+
+# Controls whether the matrix-synapse container exposes the various worker ports
+# (see `port` and `metrics_port` in `matrix_synapse_workers_enabled_list`) outside of the container.
+#
+# Takes an "<ip>" value (e.g. "127.0.0.1", "0.0.0.0", etc), or empty string to not expose.
+# It takes "*" to signify "bind on all interfaces" ("0.0.0.0" is IPv4-only).
+matrix_synapse_workers_container_host_bind_address: ''
+
+matrix_synapse_workers_generic_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['generic_workers_count'] }}"
+matrix_synapse_workers_generic_workers_port_range_start: 18111
+matrix_synapse_workers_generic_workers_metrics_range_start: 19111
+
+# matrix_synapse_workers_pusher_workers_count can only be 0 or 1 for now.
+# More instances are not supported due to a playbook limitation having to do with keeping `pusher_instances` in `homeserver.yaml` updated.
+# See https://github.com/matrix-org/synapse/commit/ddfdf945064925eba761ae3748e38f3a1c73c328
+matrix_synapse_workers_pusher_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['pusher_workers_count'] }}"
+matrix_synapse_workers_pusher_workers_metrics_range_start: 19200
+
+# matrix_synapse_workers_appservice_workers_count can only be 0 or 1. More instances are not supported.
+matrix_synapse_workers_appservice_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['appservice_workers_count'] }}"
+matrix_synapse_workers_appservice_workers_metrics_range_start: 19300
+
+# matrix_synapse_workers_federation_sender_workers_count can only be 0 or 1 for now.
+# More instances are not supported due to a playbook limitation having to do with keeping `federation_sender_instances` in `homeserver.yaml` updated.
+# See https://github.com/matrix-org/synapse/blob/master/docs/workers.md#synapseappfederation_sender
+matrix_synapse_workers_federation_sender_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['federation_sender_workers_count'] }}"
+matrix_synapse_workers_federation_sender_workers_metrics_range_start: 19400
+
+matrix_synapse_workers_media_repository_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['media_repository_workers_count'] }}"
+matrix_synapse_workers_media_repository_workers_port_range_start: 18551
+matrix_synapse_workers_media_repository_workers_metrics_range_start: 19551
+
+# Disabled until https://github.com/matrix-org/synapse/issues/8787 is resolved.
+matrix_synapse_workers_user_dir_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['user_dir_workers_count'] }}"
+matrix_synapse_workers_user_dir_workers_port_range_start: 18661
+matrix_synapse_workers_user_dir_workers_metrics_range_start: 19661
+
+matrix_synapse_workers_frontend_proxy_workers_count: "{{ matrix_synapse_workers_presets[matrix_synapse_workers_preset]['frontend_proxy_workers_count'] }}"
+matrix_synapse_workers_frontend_proxy_workers_port_range_start: 18771
+matrix_synapse_workers_frontend_proxy_workers_metrics_range_start: 19771
+
+# Default list of workers to spawn.
+#
+# Unless you populate this manually, this list is dynamically generated
+# based on other variables above:
+# - `matrix_synapse_workers_*_workers_count`
+# - `matrix_synapse_workers_*_workers_port_range_start`
+# - `matrix_synapse_workers_*_workers_port_metrics_range_start`
+#
+# We advise that you use those variables and let this list be populated dynamically.
+# Doing that is simpler and also protects you from shooting yourself in the foot,
+# as certain workers can only be spawned just once.
+#
+# Each worker instance in the list defines the following fields:
+# - `type` - the type of worker (`generic_worker`, etc.)
+# - `instanceId` - a string that identifies the worker. The combination of (`type` + `instanceId`) represents the name of the worker and must be unique.
+# - `port` - an HTTP port where the worker listens for requests (can be `0` for workers that don't do HTTP request processing)
+# - `metrics_port` - an HTTP port where the worker exports Prometheus metrics
+#
+# Example of what this needs to look like, if you're defining it manually:
+# matrix_synapse_workers_enabled_list:
+#   - { type: generic_worker, instanceId: '18111', port: 18111, metrics_port: 19111 }
+#   - { type: generic_worker, instanceId: '18112', port: 18112, metrics_port: 19112 }
+#   - { type: generic_worker, instanceId: '18113', port: 18113, metrics_port: 19113 }
+#   - { type: generic_worker, instanceId: '18114', port: 18114, metrics_port: 19114 }
+#   - { type: generic_worker, instanceId: '18115', port: 18115, metrics_port: 19115 }
+#   - { type: generic_worker, instanceId: '18116', port: 18116, metrics_port: 19116 }
+#   - { type: pusher, instanceId: '0', port: 0, metrics_port: 19200 }
+#   - { type: appservice, instanceId: '0', port: 0, metrics_port: 19300 }
+#   - { type: federation_sender, instanceId: '0', port: 0, metrics_port: 19400 }
+#   - { type: media_repository, instanceId: '18551', port: 18551, metrics_port: 19551 }
+matrix_synapse_workers_enabled_list: []
+
+# Redis information
+matrix_synapse_redis_enabled: false
+matrix_synapse_redis_host: ""
+matrix_synapse_redis_port: 6379
+matrix_synapse_redis_password: ""
+
+# Controls whether Synapse starts a replication listener necessary for workers.
+#
+# If Redis is available, we prefer to use that, instead of talking over Synapse's custom replication protocol.
+#
+# matrix_synapse_replication_listener_enabled: "{{ matrix_synapse_workers_enabled and not matrix_redis_enabled }}"
+# We force-enable this listener for now until we debug why communication via Redis fails.
+matrix_synapse_replication_listener_enabled: true
+
+# Port used for communication between main synapse process and workers.
+# Only gets used if `matrix_synapse_replication_listener_enabled: true`
+matrix_synapse_replication_http_port: 9093
+
+# Send ERROR logs to sentry.io for easier tracking
+# To set this up: go to sentry.io, create a python project, and set
+# matrix_synapse_sentry_dsn to the URL it gives you.
+# See https://github.com/matrix-org/synapse/issues/4632 for important privacy concerns
+matrix_synapse_sentry_dsn: ""
+
+# Postgres database information
+matrix_synapse_database_host: "matrix-postgres"
+matrix_synapse_database_port: 5432
+matrix_synapse_database_user: "synapse"
+matrix_synapse_database_password: ""
+matrix_synapse_database_database: "synapse"
+
+matrix_synapse_turn_uris: []
+matrix_synapse_turn_shared_secret: ""
+matrix_synapse_turn_allow_guests: False
+
+matrix_synapse_email_enabled: false
+matrix_synapse_email_smtp_host: ""
+matrix_synapse_email_smtp_port: 587
+matrix_synapse_email_smtp_require_transport_security: false
+matrix_synapse_email_notif_from: "Matrix <matrix@{{ matrix_domain }}>"
+matrix_synapse_email_client_base_url: "https://{{ matrix_server_fqn_element }}"
+
+
+# Enable this to activate the REST auth password provider module.
+# See: https://github.com/ma1uta/matrix-synapse-rest-password-provider
+matrix_synapse_ext_password_provider_rest_auth_enabled: false
+matrix_synapse_ext_password_provider_rest_auth_download_url: "https://raw.githubusercontent.com/ma1uta/matrix-synapse-rest-password-provider/ed377fb70513c2e51b42055eb364195af1ccaf33/rest_auth_provider.py"
+matrix_synapse_ext_password_provider_rest_auth_endpoint: ""
+matrix_synapse_ext_password_provider_rest_auth_registration_enforce_lowercase: false
+matrix_synapse_ext_password_provider_rest_auth_registration_profile_name_autofill: true
+matrix_synapse_ext_password_provider_rest_auth_login_profile_name_autofill: false
+
+# Enable this to activate the Shared Secret Auth password provider module.
+# See: https://github.com/devture/matrix-synapse-shared-secret-auth
+matrix_synapse_ext_password_provider_shared_secret_auth_enabled: false
+matrix_synapse_ext_password_provider_shared_secret_auth_download_url: "https://raw.githubusercontent.com/devture/matrix-synapse-shared-secret-auth/1.0.2/shared_secret_authenticator.py"
+matrix_synapse_ext_password_provider_shared_secret_auth_shared_secret: ""
+
+# Enable this to activate LDAP password provider
+matrix_synapse_ext_password_provider_ldap_enabled: false
+matrix_synapse_ext_password_provider_ldap_uri: "ldap://ldap.mydomain.tld:389"
+matrix_synapse_ext_password_provider_ldap_start_tls: true
+matrix_synapse_ext_password_provider_ldap_base: ""
+matrix_synapse_ext_password_provider_ldap_attributes_uid: "uid"
+matrix_synapse_ext_password_provider_ldap_attributes_mail: "mail"
+matrix_synapse_ext_password_provider_ldap_attributes_name: "cn"
+matrix_synapse_ext_password_provider_ldap_bind_dn: ""
+matrix_synapse_ext_password_provider_ldap_bind_password: ""
+matrix_synapse_ext_password_provider_ldap_filter: ""
+matrix_synapse_ext_password_provider_ldap_active_directory: false
+matrix_synapse_ext_password_provider_ldap_default_domain: ""
+
+# Enable this to activate the Synapse Antispam spam-checker module.
+# See: https://github.com/t2bot/synapse-simple-antispam
+matrix_synapse_ext_spam_checker_synapse_simple_antispam_enabled: false
+matrix_synapse_ext_spam_checker_synapse_simple_antispam_git_repository_url: "https://github.com/t2bot/synapse-simple-antispam"
+matrix_synapse_ext_spam_checker_synapse_simple_antispam_git_version: "923ca5c85b08f157181721abbae50dd89c31e4b5"
+matrix_synapse_ext_spam_checker_synapse_simple_antispam_config_blocked_homeservers: []
+
+# Enable this to activate the Mjolnir Antispam spam-checker module.
+# See: https://github.com/matrix-org/mjolnir#synapse-module
+matrix_synapse_ext_spam_checker_mjolnir_antispam_enabled: false
+matrix_synapse_ext_spam_checker_mjolnir_antispam_git_repository_url: "https://github.com/matrix-org/mjolnir"
+matrix_synapse_ext_spam_checker_mjolnir_antispam_git_version: "70f353fbbad0af469b1001080dea194d512b2815"
+matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_invites: true
+# Flag messages sent by servers/users in the ban lists as spam. Currently
+# this means that spammy messages will appear as empty to users. Default
+# false.
+matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_messages: false
+# Remove users from the user directory search by filtering matrix IDs and
+# display names by the entries in the user ban list. Default false.
+matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_usernames: false
+# The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
+# this list cannot be room aliases or permalinks. This server is expected
+# to already be joined to the room - Mjolnir will not automatically join
+# these rooms.
+# ["!roomid:example.org"]
+matrix_synapse_ext_spam_checker_mjolnir_antispam_config_ban_lists: []
+
+
+matrix_s3_media_store_enabled: false
+matrix_s3_media_store_custom_endpoint_enabled: false
+matrix_s3_goofys_docker_image: "ewoutp/goofys:latest"
+matrix_s3_goofys_docker_image_force_pull: "{{ matrix_s3_goofys_docker_image.endswith(':latest') }}"
+matrix_s3_media_store_custom_endpoint: "your-custom-endpoint"
+matrix_s3_media_store_bucket_name: "your-bucket-name"
+matrix_s3_media_store_aws_access_key: "your-aws-access-key"
+matrix_s3_media_store_aws_secret_key: "your-aws-secret-key"
+matrix_s3_media_store_region: "eu-central-1"
+matrix_s3_media_store_path: "{{ matrix_synapse_media_store_path }}"
+
+# Controls whether the self-check feature should validate SSL certificates.
+matrix_synapse_self_check_validate_certificates: true
+
+# Controls whether searching the public room list is enabled.
+matrix_synapse_enable_room_list_search: true
+
+# Controls who's allowed to create aliases on this server.
+matrix_synapse_alias_creation_rules:
+  - user_id: "*"
+    alias: "*"
+    room_id: "*"
+    action: allow
+
+# Controls who can publish and which rooms can be published in the public room list.
+matrix_synapse_room_list_publication_rules:
+  - user_id: "*"
+    alias: "*"
+    room_id: "*"
+    action: allow
+
+matrix_synapse_default_room_version: "6"
+
+# Controls the Synapse `spam_checker` setting.
+#
+# If a spam-checker extension is enabled, this variable's value is set automatically by the playbook during runtime.
+# If not, you can also control its value manually.
+matrix_synapse_spam_checker: []
+
+matrix_synapse_trusted_key_servers:
+  - server_name: "matrix.org"
+
+matrix_synapse_redaction_retention_period: 7d
+
+matrix_synapse_user_ips_max_age: 28d
+
+
+matrix_synapse_rust_synapse_compress_state_docker_image: "devture/rust-synapse-compress-state:v0.1.0"
+matrix_synapse_rust_synapse_compress_state_docker_image_force_pull: "{{ matrix_synapse_rust_synapse_compress_state_docker_image.endswith(':latest') }}"
+
+matrix_synapse_rust_synapse_compress_state_base_path: "{{ matrix_base_data_path }}/rust-synapse-compress-state"
+
+
+# Default Synapse configuration template which covers the generic use case.
+# You can customize it by controlling the various variables inside it.
+#
+# For a more advanced customization, you can extend the default (see `matrix_synapse_configuration_extension_yaml`)
+# or completely replace this variable with your own template.
+matrix_synapse_configuration_yaml: "{{ lookup('template', 'templates/synapse/homeserver.yaml.j2') }}"
+
+matrix_synapse_configuration_extension_yaml: |
+  # Your custom YAML configuration for Synapse goes here.
+  # This configuration extends the default starting configuration (`matrix_synapse_configuration_yaml`).
+  #
+  # You can override individual variables from the default configuration, or introduce new ones.
+  #
+  # If you need something more special, you can take full control by
+  # completely redefining `matrix_synapse_configuration_yaml`.
+  #
+  # Example configuration extension follows:
+  #
+  # server_notices:
+  #   system_mxid_localpart: notices
+  #   system_mxid_display_name: "Server Notices"
+  #   system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
+  #   room_name: "Server Notices"
+
+matrix_synapse_configuration_extension: "{{ matrix_synapse_configuration_extension_yaml|from_yaml if matrix_synapse_configuration_extension_yaml|from_yaml is mapping else {} }}"
+
+# Holds the final Synapse configuration (a combination of the default and its extension).
+# You most likely don't need to touch this variable. Instead, see `matrix_synapse_configuration_yaml`.
+matrix_synapse_configuration: "{{ matrix_synapse_configuration_yaml|from_yaml|combine(matrix_synapse_configuration_extension, recursive=True) }}"
diff --git a/roles/matrix-synapse/files/workers-doc-to-yaml.awk b/roles/matrix-synapse/files/workers-doc-to-yaml.awk
new file mode 100755
index 000000000..d9295e32c
--- /dev/null
+++ b/roles/matrix-synapse/files/workers-doc-to-yaml.awk
@@ -0,0 +1,146 @@
+#!/usr/bin/awk
+# Hackish approach to get a machine-readable list of current matrix
+# synapse REST API endpoints from the official documentation at
+# https://github.com/matrix-org/synapse/raw/master/docs/workers.md
+#
+# invoke in shell with:
+# URL=https://github.com/matrix-org/synapse/raw/master/docs/workers.md
+# curl -L ${URL} | awk -f workers-doc-to-yaml.awk -
+
+function worker_stanza_append(string) {
+    worker_stanza = worker_stanza string
+}
+
+function line_is_endpoint_url(line) {
+    # probably API endpoint if it starts with white-space and ^ or /
+    return (line ~ /^ +[\^\/].*\//)
+}
+
+# Put YAML marker at beginning of file.
+BEGIN {
+    print "---"
+    endpoint_conditional_comment = "  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually\n"
+}
+
+# Enable further processing after the introductory text.
+# Read each synapse worker section as record and its lines as fields.
+/Available worker applications/ {
+    enable_parsing = 1
+    # set record separator to markdown section header
+    RS = "\n### "
+    # set field separator to newline
+    FS = "\n"
+}
+
+# Once parsing is active, this will process each section as record.
+enable_parsing {
+    # Each worker section starts with a synapse.app.X headline
+    if ($1 ~ /synapse\.app\./) {
+
+        # get rid of the backticks and extract worker type from headline
+        gsub("`", "", $1)
+        gsub("synapse.app.", "", $1)
+        worker_type = $1
+
+        # initialize empty worker stanza
+        worker_stanza = ""
+
+        # track if any endpoints are mentioned in a specific section
+        worker_has_urls = 0
+
+        # some endpoint descriptions contain flag terms
+        endpoints_seem_conditional = 0
+
+        # also, collect a list of available workers
+        workers = (workers ? workers "\n" : "") "  - " worker_type
+
+        # loop through the lines (2 - number of fields in record)
+        for (i = 2; i < NF + 1; i++) {
+            # copy line for gsub replacements
+            line = $i
+
+            # end all lines but the last with a linefeed
+            linefeed = (i < NF - 1) ? "\n" : ""
+
+            # line starts with white-space and a hash: endpoint block headline
+            if (line ~ /^ +#/) {
+
+                # copy to output verbatim, normalizing white-space
+                gsub(/^ +/, "", line)
+                worker_stanza_append("  " line linefeed)
+
+            } else if (line_is_endpoint_url(line)) {
+
+                # mark section for special output formatting
+                worker_has_urls = 1
+
+                # remove leading white-space
+                gsub(/^ +/, "", line)
+                api_endpoint_regex = line
+
+                # FIXME: https://github.com/matrix-org/synapse/issues/new
+                # munge inconsistent media_repository endpoint notation
+                if (api_endpoint_regex == "/_matrix/media/") {
+                    api_endpoint_regex = "^" line
+                }
+
+                # FIXME: https://github.com/matrix-org/synapse/issues/7530
+                # https://github.com/spantaleev/matrix-docker-ansible-deploy/pull/456#issuecomment-719015911
+                if (api_endpoint_regex == "^/_matrix/client/(r0|unstable)/auth/.*/fallback/web$") {
+                    worker_stanza_append("  # FIXME: possible bug with SSO and multiple generic workers\n")
+                    worker_stanza_append("  # see https://github.com/matrix-org/synapse/issues/7530\n")
+                    worker_stanza_append("  # " api_endpoint_regex linefeed)
+                    continue
+                }
+
+                # disable endpoints which specify complications
+                if (endpoints_seem_conditional) {
+                    # only add notice if previous line didn't match
+                    if (!line_is_endpoint_url($(i - 1))) {
+                        worker_stanza_append(endpoint_conditional_comment)
+                    }
+                    worker_stanza_append("  # " api_endpoint_regex linefeed)
+                } else {
+                    # output endpoint regex
+                    worker_stanza_append("  - " api_endpoint_regex linefeed)
+                }
+
+            # white-space only line?
+            } else if (line ~ /^ *$/) {
+
+                if (i > 3 && i < NF) {
+                    # print white-space lines unless 1st or last line in section
+                    worker_stanza_append(line linefeed)
+                }
+
+            # nothing of the above: the line is regular documentation text
+            } else {
+
+                # include this text line as comment
+                worker_stanza_append("  # " line linefeed)
+
+                # and take note of words hinting at additional conditions to be met
+                if (line ~ /(^| )[Ii]f |(^| )[Ff]or /) {
+                    endpoints_seem_conditional = 1
+                }
+            }
+        }
+
+        if (worker_has_urls) {
+            print "\nmatrix_synapse_workers_" worker_type "_endpoints:"
+            print worker_stanza
+        } else {
+            # include workers without endpoints as well for reference
+            print "\n# " worker_type " worker (no API endpoints) ["
+            print worker_stanza
+            print "# ]"
+        }
+    }
+}
+
+END {
+    print "\nmatrix_synapse_workers_avail_list:"
+    print workers | "sort"
+}
+
+# vim: tabstop=4 shiftwidth=4 expandtab autoindent
diff --git a/roles/matrix-synapse/files/workers-doc-to-yaml.sh b/roles/matrix-synapse/files/workers-doc-to-yaml.sh
new file mode 100755
index 000000000..5981523b5
--- /dev/null
+++ b/roles/matrix-synapse/files/workers-doc-to-yaml.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+# Fetch the synapse worker documentation and extract endpoint URLs
+# matrix-org/synapse master branch points to current stable release
+
+URL=https://github.com/matrix-org/synapse/raw/master/docs/workers.md
+curl -L ${URL} | awk -f workers-doc-to-yaml.awk > ../vars/workers.yml
diff --git a/roles/matrix-synapse/tasks/ext/ldap-auth/setup.yml b/roles/matrix-synapse/tasks/ext/ldap-auth/setup.yml
new file mode 100644
index 000000000..e760626dc
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/ldap-auth/setup.yml
@@ -0,0 +1,8 @@
+- set_fact:
+    matrix_synapse_password_providers_enabled: true
+
+    matrix_synapse_additional_loggers: >
+      {{ matrix_synapse_additional_loggers }}
+      +
+      {{ [{'name': 'ldap_auth_provider', 'level': 'INFO'}] }}
+  when: matrix_synapse_ext_password_provider_ldap_enabled|bool
diff --git a/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup.yml b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup.yml
new file mode 100644
index 000000000..6c45f4693
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/ext/mjolnir-antispam/setup_install.yml"
+  when: matrix_synapse_ext_spam_checker_mjolnir_antispam_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/ext/mjolnir-antispam/setup_uninstall.yml"
+  when: "not matrix_synapse_ext_spam_checker_mjolnir_antispam_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_install.yml b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_install.yml
new file mode 100644
index 000000000..a416e42ba
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_install.yml
@@ -0,0 +1,52 @@
+---
+
+- name: Ensure git installed (RedHat)
+  yum:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_os_family == 'RedHat'"
+
+- name: Ensure git installed (Debian)
+  apt:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_os_family == 'Debian'"
+
+- name: Ensure git installed (Archlinux)
+  pacman:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_distribution == 'Archlinux'"
+
+- name: Clone mjolnir-antispam git repository
+  git:
+    repo: "{{ matrix_synapse_ext_spam_checker_mjolnir_antispam_git_repository_url }}"
+    version: "{{ matrix_synapse_ext_spam_checker_mjolnir_antispam_git_version }}"
+    dest: "{{ matrix_synapse_ext_path }}/mjolnir"
+  become: true
+  become_user: "{{ matrix_user_username }}"
+
+- set_fact:
+    matrix_synapse_spam_checker: >
+      {{ matrix_synapse_spam_checker }}
+      +
+      [{
+        "module": "mjolnir.AntiSpam",
+        "config": {
+          "block_invites": {{ matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_invites }},
+          "block_messages": {{ matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_messages }},
+          "block_usernames": {{ matrix_synapse_ext_spam_checker_mjolnir_antispam_config_block_usernames }},
+          "ban_lists": {{ matrix_synapse_ext_spam_checker_mjolnir_antispam_config_ban_lists }}
+        }
+      }]
+
+    matrix_synapse_container_extra_arguments: >
+      {{ matrix_synapse_container_extra_arguments|default([]) }}
+      +
+      ["--mount type=bind,src={{ matrix_synapse_ext_path }}/mjolnir/synapse_antispam/mjolnir,dst={{ matrix_synapse_in_container_python_packages_path }}/mjolnir,ro"]
diff --git a/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_uninstall.yml b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_uninstall.yml
new file mode 100644
index 000000000..f8439a873
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/mjolnir-antispam/setup_uninstall.yml
@@ -0,0 +1,6 @@
+---
+
+- name: Ensure mjolnir-antispam doesn't exist
+  file:
+    path: "{{ matrix_synapse_ext_path }}/mjolnir"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/ext/rest-auth/setup.yml b/roles/matrix-synapse/tasks/ext/rest-auth/setup.yml
new file mode 100644
index 000000000..0270784ad
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/rest-auth/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/ext/rest-auth/setup_install.yml"
+  when: matrix_synapse_ext_password_provider_rest_auth_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/ext/rest-auth/setup_uninstall.yml"
+  when: "not matrix_synapse_ext_password_provider_rest_auth_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/ext/rest-auth/setup_install.yml b/roles/matrix-synapse/tasks/ext/rest-auth/setup_install.yml
new file mode 100644
index 000000000..634b1ca5e
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/rest-auth/setup_install.yml
@@ -0,0 +1,28 @@
+---
+
+- name: Fail if REST Auth endpoint not configured
+  fail:
+    msg: "You have enabled the REST Auth password provider, but have not configured its endpoint in the `matrix_synapse_ext_password_provider_rest_auth_endpoint` variable. Consult the documentation."
+  when: "matrix_synapse_ext_password_provider_rest_auth_endpoint == ''"
+
+- name: Download matrix-synapse-rest-auth
+  get_url:
+    url: "{{ matrix_synapse_ext_password_provider_rest_auth_download_url }}"
+    dest: "{{ matrix_synapse_ext_path }}/rest_auth_provider.py"
+    force: true
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- set_fact:
+    matrix_synapse_password_providers_enabled: true
+
+    matrix_synapse_container_extra_arguments: >
+      {{ matrix_synapse_container_extra_arguments|default([]) }}
+      +
+      ["--mount type=bind,src={{ matrix_synapse_ext_path }}/rest_auth_provider.py,dst={{ matrix_synapse_in_container_python_packages_path }}/rest_auth_provider.py,ro"]
+
+    matrix_synapse_additional_loggers: >
+      {{ matrix_synapse_additional_loggers }}
+      +
+      {{ [{'name': 'rest_auth_provider', 'level': 'INFO'}] }}
diff --git a/roles/matrix-synapse/tasks/ext/rest-auth/setup_uninstall.yml b/roles/matrix-synapse/tasks/ext/rest-auth/setup_uninstall.yml
new file mode 100644
index 000000000..be8ad600b
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/rest-auth/setup_uninstall.yml
@@ -0,0 +1,6 @@
+---
+
+- name: Ensure matrix-synapse-rest-auth doesn't exist
+  file:
+    path: "{{ matrix_synapse_ext_path }}/rest_auth_provider.py"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/ext/setup.yml b/roles/matrix-synapse/tasks/ext/setup.yml
new file mode 100644
index 000000000..31637fa97
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/setup.yml
@@ -0,0 +1,11 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/ext/rest-auth/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/ext/shared-secret-auth/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/ext/ldap-auth/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/ext/synapse-simple-antispam/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/ext/mjolnir-antispam/setup.yml"
diff --git a/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup.yml b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup.yml
new file mode 100644
index 000000000..ed8d01978
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/ext/shared-secret-auth/setup_install.yml"
+  when: matrix_synapse_ext_password_provider_shared_secret_auth_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/ext/shared-secret-auth/setup_uninstall.yml"
+  when: "not matrix_synapse_ext_password_provider_shared_secret_auth_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_install.yml b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_install.yml
new file mode 100644
index 000000000..af92041df
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_install.yml
@@ -0,0 +1,28 @@
+---
+
+- name: Fail if Shared Secret Auth secret not set
+  fail:
+    msg: "Shared Secret Auth is enabled, but no secret has been set in matrix_synapse_ext_password_provider_shared_secret_auth_shared_secret"
+  when: "matrix_synapse_ext_password_provider_shared_secret_auth_shared_secret == ''"
+
+- name: Download matrix-synapse-shared-secret-auth
+  get_url:
+    url: "{{ matrix_synapse_ext_password_provider_shared_secret_auth_download_url }}"
+    dest: "{{ matrix_synapse_ext_path }}/shared_secret_authenticator.py"
+    force: true
+    mode: 0440
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- set_fact:
+    matrix_synapse_password_providers_enabled: true
+
+    matrix_synapse_container_extra_arguments: >
+      {{ matrix_synapse_container_extra_arguments|default([]) }}
+      +
+      ["--mount type=bind,src={{ matrix_synapse_ext_path }}/shared_secret_authenticator.py,dst={{ matrix_synapse_in_container_python_packages_path }}/shared_secret_authenticator.py,ro"]
+
+    matrix_synapse_additional_loggers: >
+      {{ matrix_synapse_additional_loggers }}
+      +
+      {{ [{'name': 'shared_secret_authenticator', 'level': 'INFO'}] }}
diff --git a/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_uninstall.yml b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_uninstall.yml
new file mode 100644
index 000000000..e564909e7
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/shared-secret-auth/setup_uninstall.yml
@@ -0,0 +1,6 @@
+---
+
+- name: Ensure matrix-synapse-shared-secret-auth doesn't exist
+  file:
+    path: "{{ matrix_synapse_ext_path }}/shared_secret_authenticator.py"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup.yml b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup.yml
new file mode 100644
index 000000000..efd4a0271
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/ext/synapse-simple-antispam/setup_install.yml"
+  when: matrix_synapse_ext_spam_checker_synapse_simple_antispam_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/ext/synapse-simple-antispam/setup_uninstall.yml"
+  when: "not matrix_synapse_ext_spam_checker_synapse_simple_antispam_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_install.yml b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_install.yml
new file mode 100644
index 000000000..2599e7f1b
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_install.yml
@@ -0,0 +1,54 @@
+---
+
+- name: Fail if Synapse Simple Antispam blocked homeservers is not set
+  fail:
+    msg: "Synapse Simple Antispam is enabled, but no blocked homeservers have been set in matrix_synapse_ext_spam_checker_synapse_simple_antispam_config_blocked_homeservers"
+  when: "matrix_synapse_ext_spam_checker_synapse_simple_antispam_config_blocked_homeservers|length == 0"
+
+- name: Ensure git installed (RedHat)
+  yum:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_os_family == 'RedHat'"
+
+- name: Ensure git installed (Debian)
+  apt:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_os_family == 'Debian'"
+
+- name: Ensure git installed (Archlinux)
+  pacman:
+    name:
+      - git
+    state: present
+    update_cache: no
+  when: "ansible_distribution == 'Archlinux'"
+
+- name: Clone synapse-simple-antispam git repository
+  git:
+    repo: "{{ matrix_synapse_ext_spam_checker_synapse_simple_antispam_git_repository_url }}"
+    version: "{{ matrix_synapse_ext_spam_checker_synapse_simple_antispam_git_version }}"
+    dest: "{{ matrix_synapse_ext_path }}/synapse-simple-antispam"
+  become: true
+  become_user: "{{ matrix_user_username }}"
+
+- set_fact:
+    matrix_synapse_spam_checker: >
+      {{ matrix_synapse_spam_checker }}
+      +
+      [{
+        "module": "synapse_simple_antispam.AntiSpamInvites",
+        "config": {
+          "blocked_homeservers": {{ matrix_synapse_ext_spam_checker_synapse_simple_antispam_config_blocked_homeservers }}
+        }
+      }]
+
+    matrix_synapse_container_extra_arguments: >
+      {{ matrix_synapse_container_extra_arguments|default([]) }}
+      +
+      ["--mount type=bind,src={{ matrix_synapse_ext_path }}/synapse-simple-antispam/synapse_simple_antispam,dst={{ matrix_synapse_in_container_python_packages_path }}/synapse_simple_antispam,ro"]
diff --git a/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_uninstall.yml b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_uninstall.yml
new file mode 100644
index 000000000..14cefc72d
--- /dev/null
+++ b/roles/matrix-synapse/tasks/ext/synapse-simple-antispam/setup_uninstall.yml
@@ -0,0 +1,6 @@
+---
+
+- name: Ensure synapse-simple-antispam doesn't exist
+  file:
+    path: "{{ matrix_synapse_ext_path }}/synapse-simple-antispam"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/goofys/setup.yml b/roles/matrix-synapse/tasks/goofys/setup.yml
new file mode 100644
index 000000000..6370408d0
--- /dev/null
+++ b/roles/matrix-synapse/tasks/goofys/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/goofys/setup_install.yml"
+  when: matrix_s3_media_store_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/goofys/setup_uninstall.yml"
+  when: "not matrix_s3_media_store_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/goofys/setup_install.yml b/roles/matrix-synapse/tasks/goofys/setup_install.yml
new file mode 100644
index 000000000..b5e956148
--- /dev/null
+++ b/roles/matrix-synapse/tasks/goofys/setup_install.yml
@@ -0,0 +1,41 @@
+- name: Ensure Goofys Docker image is pulled
+  docker_image:
+    name: "{{ matrix_s3_goofys_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_s3_goofys_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_s3_goofys_docker_image_force_pull }}"
+
+# This will throw a Permission Denied error if already mounted
+- name: Check Matrix Goofys external storage mountpoint path
+  stat:
+    path: "{{ matrix_s3_media_store_path }}"
+  register: local_path_matrix_s3_media_store_path_stat
+  ignore_errors: yes
+
+- name: Ensure Matrix Goofys external storage mountpoint exists
+  file:
+    path: "{{ matrix_s3_media_store_path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "not local_path_matrix_s3_media_store_path_stat.failed and not local_path_matrix_s3_media_store_path_stat.stat.exists"
+
+- name: Ensure goofys environment variables file created
+  template:
+    src: "{{ role_path }}/templates/goofys/env-goofys.j2"
+    dest: "{{ matrix_synapse_config_dir_path }}/env-goofys"
+    owner: root
+    mode: 0600
+
+- name: Ensure matrix-goofys.service installed
+  template:
+    src: "{{ role_path }}/templates/goofys/systemd/matrix-goofys.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-goofys.service"
+    mode: 0644
+  register: matrix_goofys_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-goofys.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_goofys_systemd_service_result.changed"
diff --git a/roles/matrix-synapse/tasks/goofys/setup_uninstall.yml b/roles/matrix-synapse/tasks/goofys/setup_uninstall.yml
new file mode 100644
index 000000000..91d434569
--- /dev/null
+++ b/roles/matrix-synapse/tasks/goofys/setup_uninstall.yml
@@ -0,0 +1,33 @@
+- name: Check existence of matrix-goofys service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-goofys.service"
+  register: matrix_goofys_service_stat
+
+- name: Ensure matrix-goofys is stopped
+  service:
+    name: matrix-goofys
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_goofys_service_stat.stat.exists"
+
+- name: Ensure matrix-goofys.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-goofys.service"
+    state: absent
+  when: "matrix_goofys_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-goofys.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_goofys_service_stat.stat.exists"
+
+- name: Ensure goofys environment variables file doesn't exist
+  file:
+    path: "{{ matrix_synapse_config_dir_path }}/env-goofys"
+    state: absent
+
+- name: Ensure Goofys Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_s3_goofys_docker_image }}"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/import_media_store.yml b/roles/matrix-synapse/tasks/import_media_store.yml
new file mode 100644
index 000000000..487bcb356
--- /dev/null
+++ b/roles/matrix-synapse/tasks/import_media_store.yml
@@ -0,0 +1,83 @@
+---
+
+# Pre-checks
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `server_path_media_store` variable needs to be provided to this playbook, via --extra-vars"
+  when: "server_path_media_store is not defined or server_path_media_store.startswith('<')"
+
+- name: Fail if media store is on Amazon S3
+  fail:
+    msg: "Your media store is on Amazon S3. Due to technical limitations, restoring is not supported."
+  when: matrix_s3_media_store_enabled|bool
+
+- name: Check if the provided media store directory exists
+  stat:
+    path: "{{ server_path_media_store }}"
+  register: server_path_media_store_stat
+
+- name: Fail if provided media store directory doesn't exist on the server
+  fail:
+    msg: "{{ server_path_media_store }} cannot be found on the server"
+  when: "not server_path_media_store_stat.stat.exists or not server_path_media_store_stat.stat.isdir"
+
+- name: Check if media store contains local_content
+  stat:
+    path: "{{ server_path_media_store }}/local_content"
+  register: server_path_media_store_local_content_stat
+
+- name: Check if media store contains remote_content
+  stat:
+    path: "{{ server_path_media_store }}/remote_content"
+  register: server_path_media_store_remote_content_stat
+
+- name: Fail if media store directory doesn't look okay (lacking remote and local content)
+  fail:
+    msg: "{{ server_path_media_store }} contains neither local_content nor remote_content directories. It's most likely a mistake and is not a media store directory."
+  when: "not server_path_media_store_local_content_stat.stat.exists and not server_path_media_store_remote_content_stat.stat.exists"
+
+
+# Actual import work
+
+- name: Ensure matrix-synapse is stopped
+  service:
+    name: matrix-synapse
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+
+# This can only work with local files, not if the media store is on Amazon S3,
+# as it won't be accessible in such a case.
+- name: Ensure provided media store directory is synchronized
+  synchronize:
+    src: "{{ server_path_media_store }}/"
+    dest: "{{ matrix_synapse_media_store_path }}"
+    delete: yes
+    # It's wasteful to preserve owner/group now. We chown below anyway.
+    owner: no
+    group: no
+    times: yes
+  delegate_to: "{{ inventory_hostname }}"
+
+# This is for the generic case and fails in other cases (remote file systems),
+# because in such cases the base path (matrix_synapse_media_store_path) is a mount point.
+- name: Ensure media store permissions are correct (generic case)
+  file:
+    path: "{{ matrix_synapse_media_store_path }}"
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+    recurse: yes
+  when: "not matrix_s3_media_store_enabled|bool"
+
+# We don't chown for Goofys, because due to the way it's mounted,
+# all files become owned by whoever needs to own them.
+
+- name: Ensure Synapse is started (if it previously was)
+  service:
+    name: "{{ item }}"
+    state: started
+    daemon_reload: yes
+  when: "stopping_result.changed"
+  with_items:
+    - matrix-synapse
diff --git a/roles/matrix-synapse/tasks/init.yml b/roles/matrix-synapse/tasks/init.yml
new file mode 100644
index 000000000..bc23fc861
--- /dev/null
+++ b/roles/matrix-synapse/tasks/init.yml
@@ -0,0 +1,26 @@
+# See https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/1070
+# and https://github.com/spantaleev/matrix-docker-ansible-deploy/commit/1ab507349c752042d26def3e95884f6df8886b74#commitcomment-51108407
+- name: Fail if trying to self-build on Ansible < 2.8
+  fail:
+    msg: "To self-build the Element image, you should use Ansible 2.8 or higher. See docs/ansible.md"
+  when: "ansible_version.major == 2 and ansible_version.minor < 8 and matrix_synapse_container_image_self_build and matrix_synapse_enabled"
+
+# Unless `matrix_synapse_workers_enabled_list` is explicitly defined,
+# we'll generate it dynamically.
+- import_tasks: "{{ role_path }}/tasks/synapse/workers/init.yml"
+  when: "matrix_synapse_enabled and matrix_synapse_workers_enabled and matrix_synapse_workers_enabled_list|length == 0"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-synapse.service'] }}"
+  when: matrix_synapse_enabled|bool
+
+- name: Ensure systemd services for workers are injected
+  include_tasks: "{{ role_path }}/tasks/synapse/workers/util/inject_systemd_services_for_worker.yml"
+  with_items: "{{ matrix_synapse_workers_enabled_list }}"
+  loop_control:
+    loop_var: matrix_synapse_worker_details
+  when: matrix_synapse_enabled|bool and matrix_synapse_workers_enabled|bool
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + ['matrix-goofys.service'] }}"
+  when: matrix_s3_media_store_enabled|bool
diff --git a/roles/matrix-synapse/tasks/main.yml b/roles/matrix-synapse/tasks/main.yml
new file mode 100644
index 000000000..8bf1c563d
--- /dev/null
+++ b/roles/matrix-synapse/tasks/main.yml
@@ -0,0 +1,55 @@
+- import_tasks: "{{ role_path }}/tasks/init.yml"
+  tags:
+    - always
+
+- import_tasks: "{{ role_path }}/tasks/validate_config.yml"
+  when: run_setup|bool and matrix_synapse_enabled|bool
+  tags:
+    - setup-all
+    - setup-synapse
+
+- import_tasks: "{{ role_path }}/tasks/setup_synapse.yml"
+  when: run_setup|bool
+  tags:
+    - setup-all
+    - setup-synapse
+
+- import_tasks: "{{ role_path }}/tasks/import_media_store.yml"
+  when: run_synapse_import_media_store|bool
+  tags:
+    - import-synapse-media-store
+
+- import_tasks: "{{ role_path }}/tasks/register_user.yml"
+  when: run_synapse_register_user|bool
+  tags:
+    - register-user
+
+- import_tasks: "{{ role_path }}/tasks/self_check_client_api.yml"
+  delegate_to: 127.0.0.1
+  become: false
+  when: run_self_check|bool
+  tags:
+    - self-check
+
+- import_tasks: "{{ role_path }}/tasks/self_check_federation_api.yml"
+  delegate_to: 127.0.0.1
+  become: false
+  when: run_self_check|bool
+  tags:
+    - self-check
+
+- import_tasks: "{{ role_path }}/tasks/update_user_password.yml"
+  when: run_synapse_update_user_password|bool
+  tags:
+    - update-user-password
+
+- import_tasks: "{{ role_path }}/tasks/rust-synapse-compress-state/main.yml"
+  when: run_synapse_rust_synapse_compress_state|bool
+  tags:
+    - rust-synapse-compress-state
+
+- name: Mark matrix-synapse role as executed
+  set_fact:
+    matrix_synapse_role_executed: true
+  tags:
+   - always
diff --git a/roles/matrix-synapse/tasks/register_user.yml b/roles/matrix-synapse/tasks/register_user.yml
new file mode 100644
index 000000000..9c2a3ea04
--- /dev/null
+++ b/roles/matrix-synapse/tasks/register_user.yml
@@ -0,0 +1,31 @@
+---
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `username` variable needs to be provided to this playbook, via --extra-vars"
+  when: "username is not defined or username == '<your-username>'"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `password` variable needs to be provided to this playbook, via --extra-vars"
+  when: "password is not defined or password == '<your-password>'"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `admin` variable needs to be provided to this playbook, via --extra-vars"
+  when: "admin is not defined or admin not in ['yes', 'no']"
+
+- name: Ensure matrix-synapse is started
+  service:
+    name: matrix-synapse
+    state: started
+    daemon_reload: yes
+  register: start_result
+
+- name: Wait a while, so that Synapse can manage to start
+  pause:
+    seconds: 7
+  when: "start_result.changed"
+
+- name: Register user
+  command: "{{ matrix_local_bin_path }}/matrix-synapse-register-user {{ username|quote }} {{ password|quote }} {{ '1' if admin == 'yes' else '0' }}"
diff --git a/roles/matrix-synapse/tasks/rust-synapse-compress-state/compress_room.yml b/roles/matrix-synapse/tasks/rust-synapse-compress-state/compress_room.yml
new file mode 100644
index 000000000..46cad8083
--- /dev/null
+++ b/roles/matrix-synapse/tasks/rust-synapse-compress-state/compress_room.yml
@@ -0,0 +1,48 @@
+- debug:
+    msg: "Compressing room `{{ room_details.room_id }}` having {{ room_details.count }} state group rows"
+
+- name: Generate rust-synapse-compress-state room compression command
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_compress_room_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-rust-synapse-compress-state-compress-room
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --mount type=bind,src={{ matrix_synapse_rust_synapse_compress_state_base_path }},dst=/work
+      {{ matrix_synapse_rust_synapse_compress_state_docker_image }}
+      /synapse-compress-state -t -o /work/state-compressor.sql
+      -p "host={{ matrix_synapse_database_host }} user={{ matrix_synapse_database_user }} password={{ matrix_synapse_database_password }} dbname={{ matrix_synapse_database_database }}"
+      -r '{{ room_details.room_id }}'
+
+- name: Run rust-synapse-compress-state room compression command (SQL generation)
+  command: "{{ matrix_synapse_rust_synapse_compress_state_compress_room_command }}"
+  async: "{{ matrix_synapse_rust_synapse_compress_state_compress_room_time }}"
+  poll: 10
+  register: matrix_synapse_rust_synapse_compress_state_compress_room_command_result
+
+- debug: var="matrix_synapse_rust_synapse_compress_state_compress_room_command_result"
+
+- name: Generate Postgres compression SQL import command
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_psql_import_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-rust-synapse-compress-state-psql-import
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      --mount type=bind,src={{ matrix_synapse_rust_synapse_compress_state_base_path }},dst=/work,ro
+      --entrypoint=/bin/sh
+      {{ matrix_postgres_docker_image_latest }}
+      -c "cat /work/state-compressor.sql |
+      psql -v ON_ERROR_STOP=1 -h matrix-postgres -d {{ matrix_synapse_database_database }}"
+
+- name: Import compression SQL into Postgres
+  command: "{{ matrix_synapse_rust_synapse_compress_state_psql_import_command }}"
+  async: "{{ matrix_synapse_rust_synapse_compress_state_psql_import_time }}"
+  poll: 10
+  register: matrix_synapse_rust_synapse_compress_state_psql_import_command_result
+
+- name: Clean up
+  file:
+    path: "{{ matrix_synapse_rust_synapse_compress_state_base_path }}/state-compressor.sql"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/rust-synapse-compress-state/main.yml b/roles/matrix-synapse/tasks/rust-synapse-compress-state/main.yml
new file mode 100644
index 000000000..106c59d5b
--- /dev/null
+++ b/roles/matrix-synapse/tasks/rust-synapse-compress-state/main.yml
@@ -0,0 +1,118 @@
+# Pre-checks
+
+- name: Fail if Postgres not enabled
+  fail:
+    msg: "Postgres via the matrix-postgres role is not enabled (`matrix_postgres_enabled`). Cannot use rust-synapse-compress-state."
+  when: "not matrix_postgres_enabled|bool"
+
+
+# Defaults
+
+- name: Set matrix_synapse_rust_synapse_compress_state_find_rooms_command_wait_time, if not provided
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_find_rooms_command_wait_time: 300
+  when: "matrix_synapse_rust_synapse_compress_state_find_rooms_command_wait_time|default('') == ''"
+
+- name: Set matrix_synapse_rust_synapse_compress_state_compress_room_time, if not provided
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_compress_room_time: 1800
+  when: "matrix_synapse_rust_synapse_compress_state_compress_room_time|default('') == ''"
+
+- name: Set matrix_synapse_rust_synapse_compress_state_psql_import_time, if not provided
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_psql_import_time: 1800
+  when: "matrix_synapse_rust_synapse_compress_state_psql_import_time|default('') == ''"
+
+- name: Set matrix_synapse_rust_synapse_compress_state_min_state_groups_required, if not provided
+  set_fact:
+    # The minimum number of state groups we're looking for before we consider a room eligible for compression.
+    # Rooms with a smaller state groups count will not be compressed.
+    matrix_synapse_rust_synapse_compress_state_min_state_groups_required: 100000
+  when: "matrix_synapse_rust_synapse_compress_state_min_state_groups_required|default('') == ''"
+
+
+# Actual compression work
+
+- name: Ensure rust-synapse-compress-state paths exist
+  file:
+    path: "{{ matrix_synapse_rust_synapse_compress_state_base_path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure rust-synapse-compress-state image is pulled
+  docker_image:
+    name: "{{ matrix_synapse_rust_synapse_compress_state_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_synapse_rust_synapse_compress_state_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_synapse_rust_synapse_compress_state_docker_image_force_pull }}"
+
+- name: Generate rust-synapse-compress-state room find command
+  set_fact:
+    matrix_synapse_rust_synapse_compress_state_find_rooms_command: >-
+      {{ matrix_host_command_docker }} run --rm --name matrix-rust-synapse-compress-state-find-rooms
+      --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+      --cap-drop=ALL
+      --network={{ matrix_docker_network }}
+      --env-file={{ matrix_postgres_base_path }}/env-postgres-psql
+      {{ matrix_postgres_docker_image_latest }}
+      psql -v ON_ERROR_STOP=1 -h matrix-postgres {{ matrix_synapse_database_database }} -c
+      'SELECT array_to_json(array_agg(row_to_json (r))) FROM (SELECT room_id, count(*) AS count FROM state_groups_state GROUP BY room_id HAVING count(*) > {{ matrix_synapse_rust_synapse_compress_state_min_state_groups_required }} ORDER BY count DESC) r;'
+
+- name: Find rooms eligible for compression with rust-synapse-compress-state
+  command: "{{ matrix_synapse_rust_synapse_compress_state_find_rooms_command }}"
+  async: "{{ matrix_synapse_rust_synapse_compress_state_find_rooms_command_wait_time }}"
+  poll: 10
+  register: matrix_synapse_rust_synapse_compress_state_find_rooms_command_result
+
+# We expect the output to be like this:
+#
+# "stdout_lines": [
+#     "                                                       array_to_json                                                        ",
+#     "----------------------------------------------------------------------------------------------------------------------------",
+#     " [{\"room_id\":\"!some-id\",\"count\":2461329},{\"room_id\":\"!another-id\",\"count\":512017}]",
+#     "(1 row)"
+# ]
+#
+# Row 3 (out of 4) contains the actual result.
+#
+# Row 3 contains a space when there's no result.
+
+- block:
+  - debug: var="matrix_synapse_rust_synapse_compress_state_find_rooms_command_result"
+
+  - name: Fail if room find result is not what we expect
+    fail:
+      msg: >-
+        Expecting 4 lines in the "find rooms" result.
+  when: "matrix_synapse_rust_synapse_compress_state_find_rooms_command_result.failed or matrix_synapse_rust_synapse_compress_state_find_rooms_command_result.stdout_lines|length != 4"
+
+- block:
+    # matrix_synapse_rust_synapse_compress_state_eligible_rooms is a list
+    # of dictionaries like this: {'room_id': '!some-id', 'count': 2461329}
+    - set_fact:
+        matrix_synapse_rust_synapse_compress_state_eligible_rooms: "{{ matrix_synapse_rust_synapse_compress_state_find_rooms_command_result.stdout_lines[2] | from_json }}"
+
+    - name: Display rooms that will be compressed
+      debug:
+        msg: >-
+          The following rooms contain more than {{ matrix_synapse_rust_synapse_compress_state_min_state_groups_required }} state group rows
+          (configurable via `matrix_synapse_rust_synapse_compress_state_min_state_groups_required`)
+          and will be compressed:
+          {{ matrix_synapse_rust_synapse_compress_state_eligible_rooms }}
+
+    - name: Compress room state
+      include_tasks: "{{ role_path }}/tasks/rust-synapse-compress-state/compress_room.yml"
+      with_items: "{{ matrix_synapse_rust_synapse_compress_state_eligible_rooms }}"
+      loop_control:
+        loop_var: room_details
+  when: "matrix_synapse_rust_synapse_compress_state_find_rooms_command_result.stdout_lines[2] != ' '"
+
+- name: Show notice about lack of rooms to compress
+  debug:
+    msg: >-
+      No rooms were found to contain more than {{ matrix_synapse_rust_synapse_compress_state_min_state_groups_required }} state group rows
+      (configurable via `matrix_synapse_rust_synapse_compress_state_min_state_groups_required`),
+      so there's nothing to compress.
+  when: "matrix_synapse_rust_synapse_compress_state_find_rooms_command_result.stdout_lines[2] == ' '"
diff --git a/roles/matrix-synapse/tasks/self_check_client_api.yml b/roles/matrix-synapse/tasks/self_check_client_api.yml
new file mode 100644
index 000000000..30244d500
--- /dev/null
+++ b/roles/matrix-synapse/tasks/self_check_client_api.yml
@@ -0,0 +1,21 @@
+---
+
+- name: Check Matrix Client API
+  uri:
+    url: "{{ matrix_synapse_client_api_url_endpoint_public }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_synapse_self_check_validate_certificates }}"
+  register: result_matrix_synapse_client_api
+  ignore_errors: true
+  check_mode: no
+  when: matrix_synapse_enabled|bool
+
+- name: Fail if Matrix Client API not working
+  fail:
+    msg: "Failed checking Matrix Client API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_synapse_client_api_url_endpoint_public }}`). Is Synapse running? Is port 443 open in your firewall? Full error: {{ result_matrix_synapse_client_api }}"
+  when: "matrix_synapse_enabled|bool and (result_matrix_synapse_client_api.failed or 'json' not in result_matrix_synapse_client_api)"
+
+- name: Report working Matrix Client API
+  debug:
+    msg: "The Matrix Client API at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_synapse_client_api_url_endpoint_public }}`) is working"
+  when: matrix_synapse_enabled|bool
diff --git a/roles/matrix-synapse/tasks/self_check_federation_api.yml b/roles/matrix-synapse/tasks/self_check_federation_api.yml
new file mode 100644
index 000000000..57c9e56b1
--- /dev/null
+++ b/roles/matrix-synapse/tasks/self_check_federation_api.yml
@@ -0,0 +1,26 @@
+---
+
+- name: Check Matrix Federation API
+  uri:
+    url: "{{ matrix_synapse_federation_api_url_endpoint_public }}"
+    follow_redirects: none
+    validate_certs: "{{ matrix_synapse_self_check_validate_certificates }}"
+  register: result_matrix_synapse_federation_api
+  ignore_errors: true
+  check_mode: no
+  when: matrix_synapse_enabled|bool
+
+- name: Fail if Matrix Federation API not working
+  fail:
+    msg: "Failed checking Matrix Federation API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_synapse_federation_api_url_endpoint_public }}`). Is Synapse running? Is port {{ matrix_federation_public_port }} open in your firewall? Full error: {{ result_matrix_synapse_federation_api }}"
+  when: "matrix_synapse_enabled|bool and matrix_synapse_federation_enabled|bool and (result_matrix_synapse_federation_api.failed or 'json' not in result_matrix_synapse_federation_api)"
+
+- name: Fail if Matrix Federation API unexpectedly enabled
+  fail:
+      msg: "Matrix Federation API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_synapse_federation_api_url_endpoint_public }}`) despite being disabled."
+  when: "matrix_synapse_enabled|bool and not matrix_synapse_federation_enabled|bool and not result_matrix_synapse_federation_api.failed"
+
+- name: Report working Matrix Federation API
+  debug:
+    msg: "The Matrix Federation API at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_synapse_federation_api_url_endpoint_public }}`) is working"
+  when: "matrix_synapse_enabled|bool and matrix_synapse_federation_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/setup_synapse.yml b/roles/matrix-synapse/tasks/setup_synapse.yml
new file mode 100644
index 000000000..f8bc05a1c
--- /dev/null
+++ b/roles/matrix-synapse/tasks/setup_synapse.yml
@@ -0,0 +1,25 @@
+---
+
+- name: Ensure Synapse paths exist
+  file:
+    path: "{{ item.path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  with_items:
+    - { path: "{{ matrix_synapse_config_dir_path }}", when: true }
+    - { path: "{{ matrix_synapse_ext_path }}", when: true }
+    - { path: "{{ matrix_synapse_docker_src_files_path }}", when: "{{ matrix_synapse_container_image_self_build }}" }
+    # We handle matrix_synapse_media_store_path elsewhere (in ./synapse/setup_install.yml),
+    # because if it's using Goofys and it's already mounted (from before),
+    # trying to chown/chmod it here will cause trouble.
+  when: "(matrix_synapse_enabled|bool or matrix_s3_media_store_enabled|bool) and item.when"
+
+- import_tasks: "{{ role_path }}/tasks/ext/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/synapse/workers/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/synapse/setup.yml"
+
+- import_tasks: "{{ role_path }}/tasks/goofys/setup.yml"
diff --git a/roles/matrix-synapse/tasks/synapse/setup.yml b/roles/matrix-synapse/tasks/synapse/setup.yml
new file mode 100644
index 000000000..b5d27c36c
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/setup.yml
@@ -0,0 +1,7 @@
+---
+
+- import_tasks: "{{ role_path }}/tasks/synapse/setup_install.yml"
+  when: matrix_synapse_enabled|bool
+
+- import_tasks: "{{ role_path }}/tasks/synapse/setup_uninstall.yml"
+  when: "not matrix_synapse_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/synapse/setup_install.yml b/roles/matrix-synapse/tasks/synapse/setup_install.yml
new file mode 100644
index 000000000..b658cfffc
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/setup_install.yml
@@ -0,0 +1,109 @@
+---
+
+# This will throw a Permission Denied error if already mounted using fuse
+- name: Check Synapse media store path
+  stat:
+    path: "{{ matrix_synapse_media_store_path }}"
+  register: local_path_media_store_stat
+  ignore_errors: yes
+
+# This is separate and conditional, to ensure we don't execute it
+# if the path already exists or we failed to check, because it's mounted using fuse.
+- name: Ensure Synapse media store path exists
+  file:
+    path: "{{ matrix_synapse_media_store_path }}"
+    state: directory
+    mode: 0750
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+  when: "not local_path_media_store_stat.failed and not local_path_media_store_stat.stat.exists"
+
+- name: Ensure Synapse repository is present on self-build
+  git:
+    repo: "{{ matrix_synapse_container_image_self_build_repo }}"
+    dest: "{{ matrix_synapse_docker_src_files_path }}"
+    version: "{{ matrix_synapse_docker_image.split(':')[1] }}"
+    force: "yes"
+  register: matrix_synapse_git_pull_results
+  when: "matrix_synapse_container_image_self_build|bool"
+
+- name: Ensure Synapse Docker image is built
+  docker_image:
+    name: "{{ matrix_synapse_docker_image }}"
+    source: build
+    force_source: "{{ matrix_synapse_git_pull_results.changed if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_synapse_git_pull_results.changed }}"
+    build:
+      dockerfile: docker/Dockerfile
+      path: "{{ matrix_synapse_docker_src_files_path }}"
+      pull: yes
+  when: "matrix_synapse_container_image_self_build|bool"
+
+- name: Ensure Synapse Docker image is pulled
+  docker_image:
+    name: "{{ matrix_synapse_docker_image }}"
+    source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}"
+    force_source: "{{ matrix_synapse_docker_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}"
+    force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else matrix_synapse_docker_image_force_pull }}"
+  when: "not matrix_synapse_container_image_self_build"
+
+- name: Check if a Synapse signing key exists
+  stat:
+    path: "{{ matrix_synapse_config_dir_path }}/{{ matrix_server_fqn_matrix }}.signing.key"
+  register: matrix_synapse_signing_key_stat
+
+# We do this so that the signing key would get generated.
+#
+# This will also generate a default homeserver.yaml configuration file and a log configuration file.
+# We don't care about those configuraiton files, as we replace them with our own anyway (see below).
+#
+# We don't use the `docker_container` module, because using it with `cap_drop` requires
+# a very recent version, which is not available for a lot of people yet.
+- name: Generate initial Synapse config and signing key
+  command: |
+    docker run
+    --rm
+    --name=matrix-config
+    --user={{ matrix_user_uid }}:{{ matrix_user_gid }}
+    --cap-drop=ALL
+    --mount type=bind,src={{ matrix_synapse_config_dir_path }},dst=/data
+    -e UID={{ matrix_user_uid }}
+    -e GID={{ matrix_user_gid }}
+    -e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
+    -e SYNAPSE_SERVER_NAME={{ matrix_server_fqn_matrix }}
+    -e SYNAPSE_REPORT_STATS=no
+    {{ matrix_synapse_docker_image }}
+    generate
+  when: "not matrix_synapse_signing_key_stat.stat.exists"
+
+- name: Ensure Synapse homeserver config installed
+  copy:
+    content: "{{ matrix_synapse_configuration|to_nice_yaml }}"
+    dest: "{{ matrix_synapse_config_dir_path }}/homeserver.yaml"
+    mode: 0644
+    owner: "{{ matrix_user_username }}"
+    group: "{{ matrix_user_groupname }}"
+
+- name: Ensure Synapse log config installed
+  template:
+    src: "{{ matrix_synapse_template_synapse_log }}"
+    dest: "{{ matrix_synapse_config_dir_path }}/{{ matrix_server_fqn_matrix }}.log.config"
+    mode: 0644
+
+- name: Ensure matrix-synapse.service installed
+  template:
+    src: "{{ role_path }}/templates/synapse/systemd/matrix-synapse.service.j2"
+    dest: "{{ matrix_systemd_path }}/matrix-synapse.service"
+    mode: 0644
+  register: matrix_synapse_systemd_service_result
+
+- name: Ensure systemd reloaded after matrix-synapse.service installation
+  service:
+    daemon_reload: yes
+  when: "matrix_synapse_systemd_service_result.changed"
+
+- name: Ensure matrix-synapse-register-user script created
+  template:
+    src: "{{ role_path }}/templates/synapse/usr-local-bin/matrix-synapse-register-user.j2"
+    dest: "{{ matrix_local_bin_path }}/matrix-synapse-register-user"
+    mode: 0755
diff --git a/roles/matrix-synapse/tasks/synapse/setup_uninstall.yml b/roles/matrix-synapse/tasks/synapse/setup_uninstall.yml
new file mode 100644
index 000000000..f1cdf1670
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/setup_uninstall.yml
@@ -0,0 +1,28 @@
+- name: Check existence of matrix-synapse service
+  stat:
+    path: "{{ matrix_systemd_path }}/matrix-synapse.service"
+  register: matrix_synapse_service_stat
+
+- name: Ensure matrix-synapse is stopped
+  service:
+    name: matrix-synapse
+    state: stopped
+    daemon_reload: yes
+  register: stopping_result
+  when: "matrix_synapse_service_stat.stat.exists"
+
+- name: Ensure matrix-synapse.service doesn't exist
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-synapse.service"
+    state: absent
+  when: "matrix_synapse_service_stat.stat.exists"
+
+- name: Ensure systemd reloaded after matrix-synapse.service removal
+  service:
+    daemon_reload: yes
+  when: "matrix_synapse_service_stat.stat.exists"
+
+- name: Ensure Synapse Docker image doesn't exist
+  docker_image:
+    name: "{{ matrix_synapse_docker_image }}"
+    state: absent
diff --git a/roles/matrix-synapse/tasks/synapse/workers/init.yml b/roles/matrix-synapse/tasks/synapse/workers/init.yml
new file mode 100644
index 000000000..c6fc32c30
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/init.yml
@@ -0,0 +1,86 @@
+# Below is a huge hack for dynamically building a list of workers and finally assigning it to `matrix_synapse_workers_enabled_list`.
+#
+# set_fact within a loop does not work reliably in Ansible (it only executes on the first iteration for some reason),
+# so we're forced to do something much uglier.
+
+- name: Build generic workers
+  set_fact:
+    worker:
+      type: 'generic_worker'
+      instanceId: "{{ matrix_synapse_workers_generic_workers_port_range_start + item }}"
+      port: "{{ matrix_synapse_workers_generic_workers_port_range_start + item }}"
+      metrics_port: "{{ matrix_synapse_workers_generic_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_generic_workers"
+  loop: "{{ range(0, matrix_synapse_workers_generic_workers_count|int)|list }}"
+
+- name: Build federation sender workers
+  set_fact:
+    worker:
+      type: 'federation_sender'
+      instanceId: "{{ item }}"
+      port: 0
+      metrics_port: "{{ matrix_synapse_workers_federation_sender_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_federation_sender_workers"
+  loop: "{{ range(0, matrix_synapse_workers_federation_sender_workers_count|int)|list }}"
+
+# This type of worker can only have a count of 1, at most
+- name: Build pusher workers
+  set_fact:
+    worker:
+      type: 'pusher'
+      instanceId: "{{ item }}"
+      port: 0
+      metrics_port: "{{ matrix_synapse_workers_pusher_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_pusher_workers"
+  loop: "{{ range(0, matrix_synapse_workers_pusher_workers_count|int)|list }}"
+
+# This type of worker can only have a count of 1, at most
+- name: Build appservice workers
+  set_fact:
+    worker:
+      type: 'appservice'
+      instanceId: "{{ item }}"
+      port: 0
+      metrics_port: "{{ matrix_synapse_workers_appservice_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_appservice_workers"
+  loop: "{{ range(0, matrix_synapse_workers_appservice_workers_count|int)|list }}"
+
+- name: Build media_repository workers
+  set_fact:
+    worker:
+      type: 'media_repository'
+      instanceId: "{{ matrix_synapse_workers_media_repository_workers_port_range_start + item }}"
+      port: "{{ matrix_synapse_workers_media_repository_workers_port_range_start + item }}"
+      metrics_port: "{{ matrix_synapse_workers_media_repository_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_media_repository_workers"
+  loop: "{{ range(0, matrix_synapse_workers_media_repository_workers_count|int)|list }}"
+
+- name: Build frontend_proxy workers
+  set_fact:
+    worker:
+      type: 'frontend_proxy'
+      instanceId: "{{ matrix_synapse_workers_frontend_proxy_workers_port_range_start + item }}"
+      port: "{{ matrix_synapse_workers_frontend_proxy_workers_port_range_start + item }}"
+      metrics_port: "{{ matrix_synapse_workers_frontend_proxy_workers_metrics_range_start + item }}"
+  register: "matrix_synapse_workers_list_results_frontend_proxy_workers"
+  loop: "{{ range(0, matrix_synapse_workers_frontend_proxy_workers_count|int)|list }}"
+
+- set_fact:
+    matrix_synapse_dynamic_workers_list: "{{ matrix_synapse_dynamic_workers_list|default([]) + [item.ansible_facts.worker] }}"
+  with_items: |
+    {{
+      matrix_synapse_workers_list_results_generic_workers.results
+      +
+      matrix_synapse_workers_list_results_federation_sender_workers.results
+      +
+      matrix_synapse_workers_list_results_pusher_workers.results
+      +
+      matrix_synapse_workers_list_results_appservice_workers.results
+      +
+      matrix_synapse_workers_list_results_media_repository_workers.results
+      +
+      matrix_synapse_workers_list_results_frontend_proxy_workers.results
+    }}
+
+- set_fact:
+    matrix_synapse_workers_enabled_list: "{{ matrix_synapse_dynamic_workers_list }}"
diff --git a/roles/matrix-synapse/tasks/synapse/workers/setup.yml b/roles/matrix-synapse/tasks/synapse/workers/setup.yml
new file mode 100644
index 000000000..ce66a2e40
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/setup.yml
@@ -0,0 +1,21 @@
+---
+
+# A previous version of the worker setup used this.
+# This is a temporary cleanup for people who ran that version.
+- name: Ensure old matrix-synapse.service.wants directory is gone
+  file:
+    path: "{{ matrix_systemd_path }}/matrix-synapse.service.wants"
+    state: absent
+
+# Same. This was part of a previous version of the worker setup.
+# No longer necessary.
+- name: Ensure matrix-synapse-worker-write-pid script is removed
+  file:
+    path: "{{ matrix_local_bin_path }}/matrix-synapse-worker-write-pid"
+    state: absent
+
+- include_tasks: "{{ role_path }}/tasks/synapse/workers/setup_install.yml"
+  when: "matrix_synapse_enabled|bool and matrix_synapse_workers_enabled|bool"
+
+- include_tasks: "{{ role_path }}/tasks/synapse/workers/setup_uninstall.yml"
+  when: "not matrix_synapse_workers_enabled|bool"
diff --git a/roles/matrix-synapse/tasks/synapse/workers/setup_install.yml b/roles/matrix-synapse/tasks/synapse/workers/setup_install.yml
new file mode 100644
index 000000000..983f1876f
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/setup_install.yml
@@ -0,0 +1,42 @@
+---
+
+- name: Determine current worker configs
+  find:
+    path: "{{ matrix_synapse_config_dir_path }}"
+    patterns: "worker.*.yaml"
+    use_regex: true
+  register: matrix_synapse_workers_current_config_files
+
+# This also deletes some things which we need. They will be recreated below.
+- name: Ensure previous worker configs are cleaned
+  file:
+    path: "{{ item.path }}"
+    state: absent
+  with_items: "{{ matrix_synapse_workers_current_config_files.files }}"
+
+- name: Determine current worker systemd services
+  find:
+    path: "{{ matrix_systemd_path }}"
+    patterns: "matrix-synapse-worker.*.service"
+    use_regex: true
+  register: matrix_synapse_workers_current_systemd_services
+
+- name: Ensure unnecessary worker systemd services are stopped and disabled
+  service:
+    name: "{{ item.path|basename }}"
+    state: stopped
+    enabled: false
+  with_items: "{{ matrix_synapse_workers_current_systemd_services.files }}"
+  when: "not ansible_check_mode and item.path|basename not in matrix_systemd_services_list"
+
+- name: Ensure unnecessary worker systemd services are cleaned
+  file:
+    path: "{{ item.path }}"
+    state: absent
+  with_items: "{{ matrix_synapse_workers_current_systemd_services.files }}"
+
+- name: Ensure creation of worker systemd service files and configuration files
+  include_tasks: "{{ role_path }}/tasks/synapse/workers/util/setup_files_for_worker.yml"
+  with_items: "{{ matrix_synapse_workers_enabled_list }}"
+  loop_control:
+    loop_var: matrix_synapse_worker_details
diff --git a/roles/matrix-synapse/tasks/synapse/workers/setup_uninstall.yml b/roles/matrix-synapse/tasks/synapse/workers/setup_uninstall.yml
new file mode 100644
index 000000000..4a90bfa63
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/setup_uninstall.yml
@@ -0,0 +1,36 @@
+---
+
+- name: Populate service facts
+  service_facts:
+
+- name: Ensure any worker services are stopped
+  service:
+    name: "{{ item.key }}"
+    state: stopped
+  with_dict: "{{ ansible_facts.services|default({})|dict2items|selectattr('key', 'match', 'matrix-synapse-worker-.+\\.service')|list|items2dict }}"
+
+- name: Find worker configs to be cleaned
+  find:
+    path: "{{ matrix_synapse_config_dir_path }}"
+    patterns: "worker.*.yaml"
+    use_regex: true
+  register: matrix_synapse_workers_current_config_files
+
+- name: Ensure previous worker configs are cleaned
+  file:
+    path: "{{ item.path }}"
+    state: absent
+  with_items: "{{ matrix_synapse_workers_current_config_files.files }}"
+
+- name: Find worker systemd services to be cleaned
+  find:
+    path: "{{ matrix_systemd_path }}"
+    patterns: "matrix-synapse-worker.*.service"
+    use_regex: true
+  register: matrix_synapse_workers_current_systemd_services
+
+- name: Ensure previous worker systemd services are cleaned
+  file:
+    path: "{{ item.path }}"
+    state: absent
+  with_items: "{{ matrix_synapse_workers_current_systemd_services.files }}"
diff --git a/roles/matrix-synapse/tasks/synapse/workers/util/inject_systemd_services_for_worker.yml b/roles/matrix-synapse/tasks/synapse/workers/util/inject_systemd_services_for_worker.yml
new file mode 100644
index 000000000..62b426257
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/util/inject_systemd_services_for_worker.yml
@@ -0,0 +1,18 @@
+# The tasks below run before `validate_config.yml`.
+# To avoid failing with a cryptic error message, we'll do validation here.
+#
+# This check is mostly relevant to people who explicitly define `matrix_synapse_workers_enabled_list`
+# (Synapse Workers users from the earlier days of this PR - https://github.com/spantaleev/matrix-docker-ansible-deploy/pull/456).
+#
+# In the future, it should be possible to remove this check.
+# Our own code which dynamically builds `matrix_synapse_workers_enabled_list` does things right.
+- name: Fail if instanceId not defined for worker
+  fail:
+    msg: "Synapse workers (like {{ matrix_synapse_worker_details|to_json }}) need to define an instanceId property (type + instanceId must be unique)"
+  when: "'instanceId' not in matrix_synapse_worker_details"
+
+- set_fact:
+    matrix_synapse_worker_systemd_service_name: "matrix-synapse-worker-{{ matrix_synapse_worker_details.type }}-{{ matrix_synapse_worker_details.instanceId }}.service"
+
+- set_fact:
+    matrix_systemd_services_list: "{{ matrix_systemd_services_list + [matrix_synapse_worker_systemd_service_name] }}"
diff --git a/roles/matrix-synapse/tasks/synapse/workers/util/setup_files_for_worker.yml b/roles/matrix-synapse/tasks/synapse/workers/util/setup_files_for_worker.yml
new file mode 100644
index 000000000..93ed65751
--- /dev/null
+++ b/roles/matrix-synapse/tasks/synapse/workers/util/setup_files_for_worker.yml
@@ -0,0 +1,19 @@
+- set_fact:
+    matrix_synapse_worker_systemd_service_name: "matrix-synapse-worker-{{ matrix_synapse_worker_details.type }}-{{ matrix_synapse_worker_details.instanceId }}"
+
+- set_fact:
+    matrix_synapse_worker_container_name: "{{ matrix_synapse_worker_systemd_service_name }}"
+
+- set_fact:
+    matrix_synapse_worker_config_file_name: "worker.{{ matrix_synapse_worker_details.type }}_{{ matrix_synapse_worker_details.instanceId }}.yaml"
+
+- name: Ensure configuration exists for {{ matrix_synapse_worker_systemd_service_name }}
+  template:
+    src: "{{ role_path }}/templates/synapse/worker.yaml.j2"
+    dest: "{{ matrix_synapse_config_dir_path }}/{{ matrix_synapse_worker_config_file_name }}"
+
+- name: Ensure systemd service exists for {{ matrix_synapse_worker_systemd_service_name }}
+  template:
+    src: "{{ role_path }}/templates/synapse/systemd/matrix-synapse-worker.service.j2"
+    dest: "{{ matrix_systemd_path }}/{{ matrix_synapse_worker_systemd_service_name }}.service"
+    mode: 0644
diff --git a/roles/matrix-synapse/tasks/update_user_password.yml b/roles/matrix-synapse/tasks/update_user_password.yml
new file mode 100644
index 000000000..78136785a
--- /dev/null
+++ b/roles/matrix-synapse/tasks/update_user_password.yml
@@ -0,0 +1,43 @@
+---
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `username` variable needs to be provided to this playbook, via --extra-vars"
+  when: "username is not defined or username == '<your-username>'"
+
+- name: Fail if playbook called incorrectly
+  fail:
+    msg: "The `password` variable needs to be provided to this playbook, via --extra-vars"
+  when: "password is not defined or password == '<your-password>'"
+
+- name: Fail if not using matrix-postgres container
+  fail:
+    msg: "This command is working only when matrix-postgres container is being used"
+  when: "not matrix_postgres_enabled|bool"
+
+- name: Ensure matrix-synapse is started
+  service:
+    name: matrix-synapse
+    state: started
+    daemon_reload: yes
+  register: start_result
+
+- name: Ensure matrix-postgres is started
+  service:
+    name: matrix-postgres
+    state: started
+    daemon_reload: yes
+  register: postgres_start_result
+
+
+- name: Wait a while, so that Matrix Synapse can manage to start
+  pause:
+    seconds: 7
+  when: "start_result.changed or postgres_start_result.changed"
+
+- name: Generate password hash
+  shell: "{{ matrix_host_command_docker }} exec matrix-synapse /usr/local/bin/hash_password -c /data/homeserver.yaml -p {{ password|quote }}"
+  register: password_hash
+
+- name: Update user password hash
+  command: "{{ matrix_local_bin_path }}/matrix-postgres-update-user-password-hash {{ username|quote }} {{ password_hash.stdout|quote }}"
diff --git a/roles/matrix-synapse/tasks/validate_config.yml b/roles/matrix-synapse/tasks/validate_config.yml
new file mode 100644
index 000000000..6dcb50ce5
--- /dev/null
+++ b/roles/matrix-synapse/tasks/validate_config.yml
@@ -0,0 +1,59 @@
+---
+
+- name: Fail if required Synapse settings not defined
+  fail:
+    msg: >-
+      You need to define a required configuration setting (`{{ item }}`) for using Synapse.
+  when: "vars[item] == ''"
+  with_items:
+    - "matrix_synapse_macaroon_secret_key"
+    - "matrix_synapse_database_host"
+    - "matrix_synapse_database_user"
+    - "matrix_synapse_database_password"
+    - "matrix_synapse_database_database"
+
+- name: Fail if asking for more than 1 instance of single-instance workers
+  fail:
+    msg: >-
+      `{{ item }}` cannot be more than 1. This is a single-instance worker.
+  when: "vars[item]|int > 1"
+  with_items:
+    - "matrix_synapse_workers_appservice_workers_count"
+    - "matrix_synapse_workers_pusher_workers_count"
+    - "matrix_synapse_workers_federation_sender_workers_count"
+
+- name: (Deprecation) Catch and report renamed settings
+  fail:
+    msg: >-
+      Your configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in vars"
+  with_items:
+    - {'old': 'matrix_synapse_email_riot_base_url', 'new': '<superseded by client_base_url>'}
+    - {'old': 'matrix_synapse_container_expose_api_port', 'new': '<superseded by matrix_synapse_container_federation_api_plain_host_bind_port>'}
+    - {'old': 'matrix_synapse_no_tls', 'new': '<removed>'}
+    - {'old': 'matrix_enable_room_list_search', 'new': 'matrix_synapse_enable_room_list_search'}
+    - {'old': 'matrix_alias_creation_rules', 'new': 'matrix_synapse_alias_creation_rules'}
+    - {'old': 'matrix_room_list_publication_rules', 'new': 'matrix_synapse_room_list_publication_rules'}
+    - {'old': 'matrix_synapse_rc_messages_per_second', 'new': '<per_second subkey of matrix_synapse_rc_message>'}
+    - {'old': 'matrix_synapse_rc_message_burst_count', 'new': '<burst_count subkey of matrix_synapse_rc_message>'}
+    - {'old': 'matrix_synapse_federation_rc_window_size', 'new': '<window_size subkey of matrix_synapse_rc_federation>'}
+    - {'old': 'matrix_synapse_federation_rc_sleep_limit', 'new': '<sleep_limit subkey of matrix_synapse_rc_federation>'}
+    - {'old': 'matrix_synapse_federation_rc_sleep_delay', 'new': '<sleep_delay subkey of matrix_synapse_rc_federation>'}
+    - {'old': 'matrix_synapse_federation_rc_reject_limit', 'new': '<reject_limit subkey of matrix_synapse_rc_federation>'}
+    - {'old': 'matrix_synapse_federation_rc_concurrent', 'new': '<concurrent subkey of matrix_synapse_rc_federation>'}
+    - {'old': 'matrix_synapse_container_expose_client_api_port', 'new': '<superseded by matrix_synapse_container_client_api_host_bind_port>'}
+    - {'old': 'matrix_synapse_container_expose_federation_api_port', 'new': '<superseded by matrix_synapse_container_federation_api_plain_host_bind_port>'}
+    - {'old': 'matrix_synapse_container_expose_metrics_port', 'new': '<superseded by matrix_synapse_container_metrics_api_host_bind_port>'}
+    - {'old': 'matrix_synapse_cache_factor', 'new': 'matrix_synapse_caches_global_factor'}
+    - {'old': 'matrix_synapse_trusted_third_party_id_servers', 'new': '<deprecated in Synapse v0.99.4 and removed in Synapse v1.19.0>'}
+    - {'old': 'matrix_synapse_use_presence', 'new': 'matrix_synapse_presence_enabled'}
+
+- name: (Deprecation) Catch and report renamed settings in matrix_synapse_configuration_extension_yaml
+  fail:
+    msg: >-
+      Your matrix_synapse_configuration_extension_yaml configuration contains a variable, which now has a different name.
+      Please change your configuration to rename the variable (`{{ item.old }}` -> `{{ item.new }}`).
+  when: "item.old in matrix_synapse_configuration_extension"
+  with_items:
+    - {'old': 'federation_ip_range_blacklist', 'new': 'ip_range_blacklist'}
diff --git a/roles/matrix-synapse/templates/goofys/env-goofys.j2 b/roles/matrix-synapse/templates/goofys/env-goofys.j2
new file mode 100644
index 000000000..2955efdd8
--- /dev/null
+++ b/roles/matrix-synapse/templates/goofys/env-goofys.j2
@@ -0,0 +1,3 @@
+#jinja2: lstrip_blocks: "True"
+AWS_ACCESS_KEY={{ matrix_s3_media_store_aws_access_key }}
+AWS_SECRET_KEY={{ matrix_s3_media_store_aws_secret_key }}
diff --git a/roles/matrix-synapse/templates/goofys/systemd/matrix-goofys.service.j2 b/roles/matrix-synapse/templates/goofys/systemd/matrix-goofys.service.j2
new file mode 100644
index 000000000..df4a4f23a
--- /dev/null
+++ b/roles/matrix-synapse/templates/goofys/systemd/matrix-goofys.service.j2
@@ -0,0 +1,39 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Matrix Goofys media store
+After=docker.service
+Requires=docker.service
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_docker }} kill %n
+ExecStartPre=-{{ matrix_host_command_docker }} rm %n
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name %n \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--mount type=bind,src=/etc/passwd,dst=/etc/passwd,ro \
+			--mount type=bind,src=/etc/group,dst=/etc/group,ro \
+			--mount type=bind,src={{ matrix_s3_media_store_path }},dst=/s3,bind-propagation=shared \
+			--security-opt apparmor:unconfined \
+			--cap-add mknod \
+			--cap-add sys_admin \
+			--device=/dev/fuse \
+			--env-file={{ matrix_synapse_config_dir_path }}/env-goofys \
+			--entrypoint /bin/sh \
+			{{ matrix_s3_goofys_docker_image }} \
+			-c 'goofys -f{% if not matrix_s3_media_store_custom_endpoint_enabled %} --storage-class=STANDARD_IA{% endif %}{% if matrix_s3_media_store_custom_endpoint_enabled %} --endpoint={{ matrix_s3_media_store_custom_endpoint }}{% endif %} --region {{ matrix_s3_media_store_region }} --stat-cache-ttl 60m0s --type-cache-ttl 60m0s --dir-mode 0700 --file-mode 0700 {{ matrix_s3_media_store_bucket_name }} /s3'
+
+TimeoutStartSec=5min
+ExecStop=-{{ matrix_host_command_docker }} stop %n
+ExecStop=-{{ matrix_host_command_docker }} kill %n
+ExecStop=-{{ matrix_host_command_docker }} rm %n
+ExecStop=-{{ matrix_host_command_fusermount }} -u {{ matrix_s3_media_store_path }}
+Restart=always
+RestartSec=5
+SyslogIdentifier=matrix-goofys
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-synapse/templates/synapse/homeserver.yaml.j2 b/roles/matrix-synapse/templates/synapse/homeserver.yaml.j2
new file mode 100644
index 000000000..f3d0734b5
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/homeserver.yaml.j2
@@ -0,0 +1,2937 @@
+#jinja2: lstrip_blocks: "True"
+# Configuration file for Synapse.
+#
+# This is a YAML file: see [1] for a quick introduction. Note in particular
+# that *indentation is important*: all the elements of a list or dictionary
+# should have the same indentation.
+#
+# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
+
+
+## Modules ##
+
+# Server admins can expand Synapse's functionality with external modules.
+#
+# See https://matrix-org.github.io/synapse/develop/modules.html for more
+# documentation on how to configure or create custom modules for Synapse.
+#
+modules:
+    # - module: my_super_module.MySuperClass
+    #   config:
+    #       do_thing: true
+    # - module: my_other_super_module.SomeClass
+    #   config: {}
+
+
+## Server ##
+
+# The public-facing domain of the server
+#
+# The server_name name will appear at the end of usernames and room addresses
+# created on this server. For example if the server_name was example.com,
+# usernames on this server would be in the format @user:example.com
+#
+# In most cases you should avoid using a matrix specific subdomain such as
+# matrix.example.com or synapse.example.com as the server_name for the same
+# reasons you wouldn't use user@email.example.com as your email address.
+# See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md
+# for information on how to host Synapse on a subdomain while preserving
+# a clean server_name.
+#
+# The server_name cannot be changed later so it is important to
+# configure this correctly before you start Synapse. It should be all
+# lowercase and may contain an explicit port.
+# Examples: matrix.org, localhost:8080
+#
+server_name: "{{ matrix_domain }}"
+
+# When running as a daemon, the file to store the pid in
+#
+pid_file: /homeserver.pid
+
+# The absolute URL to the web client which /_matrix/client will redirect
+# to if 'webclient' is configured under the 'listeners' configuration.
+#
+# This option can be also set to the filesystem path to the web client
+# which will be served at /_matrix/client/ if 'webclient' is configured
+# under the 'listeners' configuration, however this is a security risk:
+# https://github.com/matrix-org/synapse#security-note
+#
+#web_client_location: https://riot.example.com/
+
+# The public-facing base URL that clients use to access this Homeserver (not
+# including _matrix/...). This is the same URL a user might enter into the
+# 'Custom Homeserver URL' field on their client. If you use Synapse with a
+# reverse proxy, this should be the URL to reach Synapse via the proxy.
+# Otherwise, it should be the URL to reach Synapse's client HTTP listener (see
+# 'listeners' below).
+#
+public_baseurl: https://{{ matrix_server_fqn_matrix }}/
+
+# Set the soft limit on the number of file descriptors synapse can use
+# Zero is used to indicate synapse should set the soft limit to the
+# hard limit.
+#
+#soft_file_limit: 0
+
+# Presence tracking allows users to see the state (e.g online/offline)
+# of other local and remote users.
+#
+presence:
+  # Uncomment to disable presence tracking on this homeserver. This option
+  # replaces the previous top-level 'use_presence' option.
+  #
+  enabled: {{ matrix_synapse_presence_enabled|to_json }}
+
+  # Presence routers are third-party modules that can specify additional logic
+  # to where presence updates from users are routed.
+  #
+  presence_router:
+    # The custom module's class. Uncomment to use a custom presence router module.
+    #
+    #module: "my_custom_router.PresenceRouter"
+
+    # Configuration options of the custom module. Refer to your module's
+    # documentation for available options.
+    #
+    #config:
+    #  example_option: 'something'
+
+# Whether to require authentication to retrieve profile data (avatars,
+# display names) of other users through the client API. Defaults to
+# 'false'. Note that profile data is also available via the federation
+# API, unless allow_profile_lookup_over_federation is set to false.
+#
+require_auth_for_profile_requests: {{ matrix_synapse_require_auth_for_profile_requests|to_json }}
+
+# Uncomment to require a user to share a room with another user in order
+# to retrieve their profile information. Only checked on Client-Server
+# requests. Profile requests from other servers should be checked by the
+# requesting server. Defaults to 'false'.
+#
+limit_profile_requests_to_users_who_share_rooms: {{ matrix_synapse_limit_profile_requests_to_users_who_share_rooms|to_json }}
+
+# Uncomment to prevent a user's profile data from being retrieved and
+# displayed in a room until they have joined it. By default, a user's
+# profile data is included in an invite event, regardless of the values
+# of the above two settings, and whether or not the users share a server.
+# Defaults to 'true'.
+#
+include_profile_data_on_invite: {{ matrix_synapse_include_profile_data_on_invite|to_json }}
+
+# If set to 'true', removes the need for authentication to access the server's
+# public rooms directory through the client API, meaning that anyone can
+# query the room directory. Defaults to 'false'.
+#
+allow_public_rooms_without_auth: {{ matrix_synapse_allow_public_rooms_without_auth|to_json }}
+
+# If set to 'true', allows any other homeserver to fetch the server's public
+# rooms directory via federation. Defaults to 'false'.
+#
+allow_public_rooms_over_federation: {{ matrix_synapse_allow_public_rooms_over_federation|to_json }}
+
+# The default room version for newly created rooms.
+#
+# Known room versions are listed here:
+# https://matrix.org/docs/spec/#complete-list-of-room-versions
+#
+# For example, for room version 1, default_room_version should be set
+# to "1".
+#
+default_room_version: {{ matrix_synapse_default_room_version|to_json }}
+
+# The GC threshold parameters to pass to `gc.set_threshold`, if defined
+#
+#gc_thresholds: [700, 10, 10]
+
+# The minimum time in seconds between each GC for a generation, regardless of
+# the GC thresholds. This ensures that we don't do GC too frequently.
+#
+# A value of `[1s, 10s, 30s]` indicates that a second must pass between consecutive
+# generation 0 GCs, etc.
+#
+# Defaults to `[1s, 10s, 30s]`.
+#
+#gc_min_interval: [0.5s, 30s, 1m]
+
+# Set the limit on the returned events in the timeline in the get
+# and sync operations. The default value is 100. -1 means no upper limit.
+#
+# Uncomment the following to increase the limit to 5000.
+#
+#filter_timeline_limit: 5000
+
+# Whether room invites to users on this server should be blocked
+# (except those sent by local server admins). The default is False.
+#
+#block_non_admin_invites: True
+
+# Room searching
+#
+# If disabled, new messages will not be indexed for searching and users
+# will receive errors when searching for messages. Defaults to enabled.
+#
+#enable_search: false
+
+# Prevent outgoing requests from being sent to the following blacklisted IP address
+# CIDR ranges. If this option is not specified then it defaults to private IP
+# address ranges (see the example below).
+#
+# The blacklist applies to the outbound requests for federation, identity servers,
+# push servers, and for checking key validity for third-party invite events.
+#
+# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
+# listed here, since they correspond to unroutable addresses.)
+#
+# This option replaces federation_ip_range_blacklist in Synapse v1.25.0.
+#
+#ip_range_blacklist:
+#  - '127.0.0.0/8'
+#  - '10.0.0.0/8'
+#  - '172.16.0.0/12'
+#  - '192.168.0.0/16'
+#  - '100.64.0.0/10'
+#  - '192.0.0.0/24'
+#  - '169.254.0.0/16'
+#  - '192.88.99.0/24'
+#  - '198.18.0.0/15'
+#  - '192.0.2.0/24'
+#  - '198.51.100.0/24'
+#  - '203.0.113.0/24'
+#  - '224.0.0.0/4'
+#  - '::1/128'
+#  - 'fe80::/10'
+#  - 'fc00::/7'
+#  - '2001:db8::/32'
+#  - 'ff00::/8'
+#  - 'fec0::/10'
+
+# List of IP address CIDR ranges that should be allowed for federation,
+# identity servers, push servers, and for checking key validity for
+# third-party invite events. This is useful for specifying exceptions to
+# wide-ranging blacklisted target IP ranges - e.g. for communication with
+# a push server only visible in your network.
+#
+# This whitelist overrides ip_range_blacklist and defaults to an empty
+# list.
+#
+#ip_range_whitelist:
+#   - '192.168.1.1'
+
+# List of ports that Synapse should listen on, their purpose and their
+# configuration.
+#
+# Options for each listener include:
+#
+#   port: the TCP port to bind to
+#
+#   bind_addresses: a list of local addresses to listen on. The default is
+#       'all local interfaces'.
+#
+#   type: the type of listener. Normally 'http', but other valid options are:
+#       'manhole' (see docs/manhole.md),
+#       'metrics' (see docs/metrics-howto.md),
+#       'replication' (see docs/workers.md).
+#
+#   tls: set to true to enable TLS for this listener. Will use the TLS
+#       key/cert specified in tls_private_key_path / tls_certificate_path.
+#
+#   x_forwarded: Only valid for an 'http' listener. Set to true to use the
+#       X-Forwarded-For header as the client IP. Useful when Synapse is
+#       behind a reverse-proxy.
+#
+#   resources: Only valid for an 'http' listener. A list of resources to host
+#       on this port. Options for each resource are:
+#
+#       names: a list of names of HTTP resources. See below for a list of
+#           valid resource names.
+#
+#       compress: set to true to enable HTTP compression for this resource.
+#
+#   additional_resources: Only valid for an 'http' listener. A map of
+#        additional endpoints which should be loaded via dynamic modules.
+#
+# Valid resource names are:
+#
+#   client: the client-server API (/_matrix/client), and the synapse admin
+#       API (/_synapse/admin). Also implies 'media' and 'static'.
+#
+#   consent: user consent forms (/_matrix/consent). See
+#       docs/consent_tracking.md.
+#
+#   federation: the server-server API (/_matrix/federation). Also implies
+#       'media', 'keys', 'openid'
+#
+#   keys: the key discovery API (/_matrix/keys).
+#
+#   media: the media API (/_matrix/media).
+#
+#   metrics: the metrics interface. See docs/metrics-howto.md.
+#
+#   openid: OpenID authentication.
+#
+#   replication: the HTTP replication API (/_synapse/replication). See
+#       docs/workers.md.
+#
+#   static: static resources under synapse/static (/_matrix/static). (Mostly
+#       useful for 'fallback authentication'.)
+#
+#   webclient: A web client. Requires web_client_location to be set.
+#
+listeners:
+{% if matrix_synapse_metrics_enabled %}
+  - type: metrics
+    port: {{ matrix_synapse_metrics_port }}
+    bind_addresses:
+      - '0.0.0.0'
+{% endif %}
+
+{% if matrix_synapse_federation_port_enabled and matrix_synapse_tls_federation_listener_enabled %}
+  # TLS-enabled listener: for when matrix traffic is sent directly to synapse.
+  - port: 8448
+    tls: true
+    bind_addresses: ['::']
+    type: http
+    x_forwarded: false
+
+    resources:
+      - names: {{ matrix_synapse_federation_listener_resource_names|to_json }}
+        compress: false
+{% endif %}
+
+  # Unsecure HTTP listener (Client API): for when matrix traffic passes through a reverse proxy
+  # that unwraps TLS.
+  - port: 8008
+    tls: false
+    bind_addresses: ['::']
+    type: http
+    x_forwarded: true
+
+    resources:
+      - names: {{ matrix_synapse_http_listener_resource_names|to_json }}
+        compress: false
+
+{% if matrix_synapse_federation_port_enabled %}
+  # Unsecure HTTP listener (Federation API): for when matrix traffic passes through a reverse proxy
+  # that unwraps TLS.
+  - port: 8048
+    tls: false
+    bind_addresses: ['::']
+    type: http
+    x_forwarded: true
+
+    resources:
+      - names: {{ matrix_synapse_federation_listener_resource_names|to_json }}
+        compress: false
+{% endif %}
+
+{% if matrix_synapse_manhole_enabled %}
+  # Turn on the twisted ssh manhole service on localhost on the given
+  # port.
+  - port: 9000
+    bind_addresses: ['0.0.0.0']
+    type: manhole
+{% endif %}
+
+{% if matrix_synapse_workers_enabled %}
+
+{% if matrix_synapse_replication_listener_enabled %}
+  # c.f. https://github.com/matrix-org/synapse/tree/master/docs/workers.md
+  # HTTP replication: for the workers to send data to the main synapse process
+  - port: {{ matrix_synapse_replication_http_port }}
+    bind_addresses: ['0.0.0.0']
+    type: http
+    resources:
+      - names: [replication]
+{% endif %}
+
+# c.f. https://github.com/matrix-org/synapse/tree/master/contrib/systemd-with-workers/README.md
+worker_app: synapse.app.homeserver
+
+# thx https://oznetnerd.com/2017/04/18/jinja2-selectattr-filter/
+# reduce the main worker's offerings to core homeserver business
+{% if matrix_synapse_workers_enabled_list|selectattr('type', 'equalto', 'appservice')|list  %}
+notify_appservices: false
+{% endif %}
+{% if matrix_synapse_workers_enabled_list|selectattr('type', 'equalto', 'federation_sender')|list  %}
+send_federation: false
+{% endif %}
+{% if matrix_synapse_workers_enabled_list|selectattr('type', 'equalto', 'media_repository')|list  %}
+enable_media_repo: false
+{% endif %}
+{% if matrix_synapse_workers_enabled_list|selectattr('type', 'equalto', 'pusher')|list  %}
+start_pushers: false
+{% endif %}
+{% if matrix_synapse_workers_enabled_list|selectattr('type', 'equalto', 'user_dir')|list  %}
+update_user_directory: false
+{% endif %}
+
+daemonize: false
+{% endif %}
+
+# Forward extremities can build up in a room due to networking delays between
+# homeservers. Once this happens in a large room, calculation of the state of
+# that room can become quite expensive. To mitigate this, once the number of
+# forward extremities reaches a given threshold, Synapse will send an
+# org.matrix.dummy_event event, which will reduce the forward extremities
+# in the room.
+#
+# This setting defines the threshold (i.e. number of forward extremities in the
+# room) at which dummy events are sent. The default value is 10.
+#
+#dummy_events_threshold: 5
+
+
+## Homeserver blocking ##
+
+# How to reach the server admin, used in ResourceLimitError
+#
+#admin_contact: 'mailto:admin@server.com'
+
+# Global blocking
+#
+#hs_disabled: False
+#hs_disabled_message: 'Human readable reason for why the HS is blocked'
+#hs_disabled_limit_type: 'error code(str), to help clients decode reason'
+
+# Monthly Active User Blocking
+#
+# Used in cases where the admin or server owner wants to limit to the
+# number of monthly active users.
+#
+# 'limit_usage_by_mau' disables/enables monthly active user blocking. When
+# enabled and a limit is reached the server returns a 'ResourceLimitError'
+# with error type Codes.RESOURCE_LIMIT_EXCEEDED
+#
+# 'max_mau_value' is the hard limit of monthly active users above which
+# the server will start blocking user actions.
+#
+# 'mau_trial_days' is a means to add a grace period for active users. It
+# means that users must be active for this number of days before they
+# can be considered active and guards against the case where lots of users
+# sign up in a short space of time never to return after their initial
+# session.
+#
+#limit_usage_by_mau: False
+#max_mau_value: 50
+#mau_trial_days: 2
+
+# If enabled, the metrics for the number of monthly active users will
+# be populated, however no one will be limited. If limit_usage_by_mau
+# is true, this is implied to be true.
+#
+#mau_stats_only: False
+
+# Sometimes the server admin will want to ensure certain accounts are
+# never blocked by mau checking. These accounts are specified here.
+#
+#mau_limit_reserved_threepids:
+#  - medium: 'email'
+#    address: 'reserved_user@example.com'
+
+# Used by phonehome stats to group together related servers.
+#server_context: context
+
+# Resource-constrained homeserver settings
+#
+# When this is enabled, the room "complexity" will be checked before a user
+# joins a new remote room. If it is above the complexity limit, the server will
+# disallow joining, or will instantly leave.
+#
+# Room complexity is an arbitrary measure based on factors such as the number of
+# users in the room.
+#
+limit_remote_rooms:
+  # Uncomment to enable room complexity checking.
+  #
+  #enabled: true
+
+  # the limit above which rooms cannot be joined. The default is 1.0.
+  #
+  #complexity: 0.5
+
+  # override the error which is returned when the room is too complex.
+  #
+  #complexity_error: "This room is too complex."
+
+  # allow server admins to join complex rooms. Default is false.
+  #
+  #admins_can_join: true
+
+# Whether to require a user to be in the room to add an alias to it.
+# Defaults to 'true'.
+#
+#require_membership_for_aliases: false
+
+# Whether to allow per-room membership profiles through the send of membership
+# events with profile information that differ from the target's global profile.
+# Defaults to 'true'.
+#
+#allow_per_room_profiles: false
+
+# How long to keep redacted events in unredacted form in the database. After
+# this period redacted events get replaced with their redacted form in the DB.
+#
+# Defaults to `7d`. Set to `null` to disable.
+#
+#redaction_retention_period: 28d
+
+redaction_retention_period: {{ matrix_synapse_redaction_retention_period }}
+
+# How long to track users' last seen time and IPs in the database.
+#
+# Defaults to `28d`. Set to `null` to disable clearing out of old rows.
+#
+#user_ips_max_age: 14d
+
+user_ips_max_age: {{ matrix_synapse_user_ips_max_age }}
+
+# Message retention policy at the server level.
+#
+# Room admins and mods can define a retention period for their rooms using the
+# 'm.room.retention' state event, and server admins can cap this period by setting
+# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options.
+#
+# If this feature is enabled, Synapse will regularly look for and purge events
+# which are older than the room's maximum retention period. Synapse will also
+# filter events received over federation so that events that should have been
+# purged are ignored and not stored again.
+#
+retention:
+  # The message retention policies feature is disabled by default. Uncomment the
+  # following line to enable it.
+  #
+  #enabled: true
+
+  # Default retention policy. If set, Synapse will apply it to rooms that lack the
+  # 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't
+  # matter much because Synapse doesn't take it into account yet.
+  #
+  #default_policy:
+  #  min_lifetime: 1d
+  #  max_lifetime: 1y
+
+  # Retention policy limits. If set, and the state of a room contains a
+  # 'm.room.retention' event in its state which contains a 'min_lifetime' or a
+  # 'max_lifetime' that's out of these bounds, Synapse will cap the room's policy
+  # to these limits when running purge jobs.
+  #
+  #allowed_lifetime_min: 1d
+  #allowed_lifetime_max: 1y
+
+  # Server admins can define the settings of the background jobs purging the
+  # events which lifetime has expired under the 'purge_jobs' section.
+  #
+  # If no configuration is provided, a single job will be set up to delete expired
+  # events in every room daily.
+  #
+  # Each job's configuration defines which range of message lifetimes the job
+  # takes care of. For example, if 'shortest_max_lifetime' is '2d' and
+  # 'longest_max_lifetime' is '3d', the job will handle purging expired events in
+  # rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and
+  # lower than or equal to 3 days. Both the minimum and the maximum value of a
+  # range are optional, e.g. a job with no 'shortest_max_lifetime' and a
+  # 'longest_max_lifetime' of '3d' will handle every room with a retention policy
+  # which 'max_lifetime' is lower than or equal to three days.
+  #
+  # The rationale for this per-job configuration is that some rooms might have a
+  # retention policy with a low 'max_lifetime', where history needs to be purged
+  # of outdated messages on a more frequent basis than for the rest of the rooms
+  # (e.g. every 12h), but not want that purge to be performed by a job that's
+  # iterating over every room it knows, which could be heavy on the server.
+  #
+  # If any purge job is configured, it is strongly recommended to have at least
+  # a single job with neither 'shortest_max_lifetime' nor 'longest_max_lifetime'
+  # set, or one job without 'shortest_max_lifetime' and one job without
+  # 'longest_max_lifetime' set. Otherwise some rooms might be ignored, even if
+  # 'allowed_lifetime_min' and 'allowed_lifetime_max' are set, because capping a
+  # room's policy to these values is done after the policies are retrieved from
+  # Synapse's database (which is done using the range specified in a purge job's
+  # configuration).
+  #
+  #purge_jobs:
+  #  - longest_max_lifetime: 3d
+  #    interval: 12h
+  #  - shortest_max_lifetime: 3d
+  #    interval: 1d
+
+# Inhibits the /requestToken endpoints from returning an error that might leak
+# information about whether an e-mail address is in use or not on this
+# homeserver.
+# Note that for some endpoints the error situation is the e-mail already being
+# used, and for others the error is entering the e-mail being unused.
+# If this option is enabled, instead of returning an error, these endpoints will
+# act as if no error happened and return a fake session ID ('sid') to clients.
+#
+#request_token_inhibit_3pid_errors: true
+
+# A list of domains that the domain portion of 'next_link' parameters
+# must match.
+#
+# This parameter is optionally provided by clients while requesting
+# validation of an email or phone number, and maps to a link that
+# users will be automatically redirected to after validation
+# succeeds. Clients can make use this parameter to aid the validation
+# process.
+#
+# The whitelist is applied whether the homeserver or an
+# identity server is handling validation.
+#
+# The default value is no whitelist functionality; all domains are
+# allowed. Setting this value to an empty list will instead disallow
+# all domains.
+#
+#next_link_domain_whitelist: ["matrix.org"]
+
+
+## TLS ##
+
+# PEM-encoded X509 certificate for TLS.
+# This certificate, as of Synapse 1.0, will need to be a valid and verifiable
+# certificate, signed by a recognised Certificate Authority.
+#
+# Be sure to use a `.pem` file that includes the full certificate chain including
+# any intermediate certificates (for instance, if using certbot, use
+# `fullchain.pem` as your certificate, not `cert.pem`).
+#
+tls_certificate_path: {{ matrix_synapse_tls_certificate_path|to_json }}
+
+# PEM-encoded private key for TLS
+#
+tls_private_key_path: {{ matrix_synapse_tls_private_key_path|to_json }}
+
+# Whether to verify TLS server certificates for outbound federation requests.
+#
+# Defaults to `true`. To disable certificate verification, uncomment the
+# following line.
+#
+#federation_verify_certificates: false
+
+# The minimum TLS version that will be used for outbound federation requests.
+#
+# Defaults to `1`. Configurable to `1`, `1.1`, `1.2`, or `1.3`. Note
+# that setting this value higher than `1.2` will prevent federation to most
+# of the public Matrix network: only configure it to `1.3` if you have an
+# entirely private federation setup and you can ensure TLS 1.3 support.
+#
+#federation_client_minimum_tls_version: 1.2
+
+# Skip federation certificate verification on the following whitelist
+# of domains.
+#
+# This setting should only be used in very specific cases, such as
+# federation over Tor hidden services and similar. For private networks
+# of homeservers, you likely want to use a private CA instead.
+#
+# Only effective if federation_verify_certicates is `true`.
+#
+#federation_certificate_verification_whitelist:
+#  - lon.example.com
+#  - *.domain.com
+#  - *.onion
+
+# List of custom certificate authorities for federation traffic.
+#
+# This setting should only normally be used within a private network of
+# homeservers.
+#
+# Note that this list will replace those that are provided by your
+# operating environment. Certificates must be in PEM format.
+#
+#federation_custom_ca_list:
+#  - myCA1.pem
+#  - myCA2.pem
+#  - myCA3.pem
+
+
+## Federation ##
+
+# Restrict federation to the following whitelist of domains.
+# N.B. we recommend also firewalling your federation listener to limit
+# inbound federation traffic as early as possible, rather than relying
+# purely on this application-layer restriction.  If not specified, the
+# default is to whitelist everything.
+#
+#federation_domain_whitelist:
+#  - lon.example.com
+#  - nyc.example.com
+#  - syd.example.com
+{% if matrix_synapse_federation_domain_whitelist is not none %}
+{# Cannot use `|to_nice_yaml` here, as an empty list does not get serialized properly by it. #}
+federation_domain_whitelist: {{ matrix_synapse_federation_domain_whitelist|to_json }}
+{% endif %}
+
+# Report prometheus metrics on the age of PDUs being sent to and received from
+# the following domains. This can be used to give an idea of "delay" on inbound
+# and outbound federation, though be aware that any delay can be due to problems
+# at either end or with the intermediate network.
+#
+# By default, no domains are monitored in this way.
+#
+#federation_metrics_domains:
+#  - matrix.org
+#  - example.com
+
+# Uncomment to disable profile lookup over federation. By default, the
+# Federation API allows other homeservers to obtain profile data of any user
+# on this homeserver. Defaults to 'true'.
+#
+#allow_profile_lookup_over_federation: false
+
+# Uncomment to disable device display name lookup over federation. By default, the
+# Federation API allows other homeservers to obtain device display names of any user
+# on this homeserver. Defaults to 'true'.
+#
+#allow_device_name_lookup_over_federation: false
+
+
+## Caching ##
+
+# Caching can be configured through the following options.
+#
+# A cache 'factor' is a multiplier that can be applied to each of
+# Synapse's caches in order to increase or decrease the maximum
+# number of entries that can be stored.
+
+# The number of events to cache in memory. Not affected by
+# caches.global_factor.
+#
+event_cache_size: "{{ matrix_synapse_event_cache_size }}"
+
+caches:
+   # Controls the global cache factor, which is the default cache factor
+   # for all caches if a specific factor for that cache is not otherwise
+   # set.
+   #
+   # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment
+   # variable. Setting by environment variable takes priority over
+   # setting through the config file.
+   #
+   # Defaults to 0.5, which will half the size of all caches.
+   #
+   global_factor: {{ matrix_synapse_caches_global_factor }}
+
+   # A dictionary of cache name to cache factor for that individual
+   # cache. Overrides the global cache factor for a given cache.
+   #
+   # These can also be set through environment variables comprised
+   # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital
+   # letters and underscores. Setting by environment variable
+   # takes priority over setting through the config file.
+   # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0
+   #
+   # Some caches have '*' and other characters that are not
+   # alphanumeric or underscores. These caches can be named with or
+   # without the special characters stripped. For example, to specify
+   # the cache factor for `*stateGroupCache*` via an environment
+   # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`.
+   #
+   per_cache_factors:
+     #get_users_who_share_room_with_user: 2.0
+
+
+## Database ##
+
+database:
+  # The database engine name
+  name: "psycopg2"
+  args:
+    user: {{ matrix_synapse_database_user|string|to_json }}
+    password: {{ matrix_synapse_database_password|string|to_json }}
+    database: "{{ matrix_synapse_database_database }}"
+    host: "{{ matrix_synapse_database_host }}"
+    port: {{ matrix_synapse_database_port }}
+    cp_min: 5
+    cp_max: 10
+
+
+## Logging ##
+
+# A yaml python logging config file as described by
+# https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
+#
+log_config: "/data/{{ matrix_server_fqn_matrix }}.log.config"
+
+
+## Ratelimiting ##
+
+# Ratelimiting settings for client actions (registration, login, messaging).
+#
+# Each ratelimiting configuration is made of two parameters:
+#   - per_second: number of requests a client can send per second.
+#   - burst_count: number of requests a client can send before being throttled.
+#
+# Synapse currently uses the following configurations:
+#   - one for messages that ratelimits sending based on the account the client
+#     is using
+#   - one for registration that ratelimits registration requests based on the
+#     client's IP address.
+#   - one for login that ratelimits login requests based on the client's IP
+#     address.
+#   - one for login that ratelimits login requests based on the account the
+#     client is attempting to log into.
+#   - one for login that ratelimits login requests based on the account the
+#     client is attempting to log into, based on the amount of failed login
+#     attempts for this account.
+#   - one for ratelimiting redactions by room admins. If this is not explicitly
+#     set then it uses the same ratelimiting as per rc_message. This is useful
+#     to allow room admins to deal with abuse quickly.
+#   - two for ratelimiting number of rooms a user can join, "local" for when
+#     users are joining rooms the server is already in (this is cheap) vs
+#     "remote" for when users are trying to join rooms not on the server (which
+#     can be more expensive)
+#   - one for ratelimiting how often a user or IP can attempt to validate a 3PID.
+#   - two for ratelimiting how often invites can be sent in a room or to a
+#     specific user.
+#
+# The defaults are as shown below.
+#
+#rc_message:
+#  per_second: 0.2
+#  burst_count: 10
+rc_message: {{ matrix_synapse_rc_message|to_json }}
+#
+#rc_registration:
+#  per_second: 0.17
+#  burst_count: 3
+rc_registration: {{ matrix_synapse_rc_registration|to_json }}
+#
+#rc_login:
+#  address:
+#    per_second: 0.17
+#    burst_count: 3
+#  account:
+#    per_second: 0.17
+#    burst_count: 3
+#  failed_attempts:
+#    per_second: 0.17
+#    burst_count: 3
+rc_login: {{ matrix_synapse_rc_login|to_json }}
+#
+#rc_admin_redaction:
+#  per_second: 1
+#  burst_count: 50
+rc_admin_redaction: {{ matrix_synapse_rc_admin_redaction|to_json }}
+#
+#rc_joins:
+#  local:
+#    per_second: 0.1
+#    burst_count: 10
+#  remote:
+#    per_second: 0.01
+#    burst_count: 10
+rc_joins: {{ matrix_synapse_rc_joins|to_json }}
+#
+#rc_3pid_validation:
+#  per_second: 0.003
+#  burst_count: 5
+#
+#rc_invites:
+#  per_room:
+#    per_second: 0.3
+#    burst_count: 10
+#  per_user:
+#    per_second: 0.003
+#    burst_count: 5
+
+# Ratelimiting settings for incoming federation
+#
+# The rc_federation configuration is made up of the following settings:
+#   - window_size: window size in milliseconds
+#   - sleep_limit: number of federation requests from a single server in
+#     a window before the server will delay processing the request.
+#   - sleep_delay: duration in milliseconds to delay processing events
+#     from remote servers by if they go over the sleep limit.
+#   - reject_limit: maximum number of concurrent federation requests
+#     allowed from a single server
+#   - concurrent: number of federation requests to concurrently process
+#     from a single server
+#
+# The defaults are as shown below.
+#
+#rc_federation:
+#  window_size: 1000
+#  sleep_limit: 10
+#  sleep_delay: 500
+#  reject_limit: 50
+#  concurrent: 3
+rc_federation: {{ matrix_synapse_rc_federation|to_json }}
+
+# Target outgoing federation transaction frequency for sending read-receipts,
+# per-room.
+#
+# If we end up trying to send out more read-receipts, they will get buffered up
+# into fewer transactions.
+#
+#federation_rr_transactions_per_room_per_second: 50
+federation_rr_transactions_per_room_per_second: {{ matrix_synapse_federation_rr_transactions_per_room_per_second }}
+
+
+
+## Media Store ##
+
+# Enable the media store service in the Synapse master. Uncomment the
+# following if you are using a separate media store worker.
+#
+#enable_media_repo: false
+
+# Directory where uploaded images and attachments are stored.
+#
+media_store_path: "/matrix-media-store-parent/{{ matrix_synapse_media_store_directory_name }}"
+
+# Media storage providers allow media to be stored in different
+# locations.
+#
+#media_storage_providers:
+#  - module: file_system
+#    # Whether to store newly uploaded local files
+#    store_local: false
+#    # Whether to store newly downloaded remote files
+#    store_remote: false
+#    # Whether to wait for successful storage for local uploads
+#    store_synchronous: false
+#    config:
+#       directory: /mnt/some/other/directory
+
+# The largest allowed upload size in bytes
+#
+# If you are using a reverse proxy you may also need to set this value in
+# your reverse proxy's config. Notably Nginx has a small max body size by default.
+# See https://matrix-org.github.io/synapse/develop/reverse_proxy.html.
+#
+max_upload_size: "{{ matrix_synapse_max_upload_size_mb }}M"
+
+# Maximum number of pixels that will be thumbnailed
+#
+#max_image_pixels: 32M
+
+# Whether to generate new thumbnails on the fly to precisely match
+# the resolution requested by the client. If true then whenever
+# a new resolution is requested by the client the server will
+# generate a new thumbnail. If false the server will pick a thumbnail
+# from a precalculated list.
+#
+#dynamic_thumbnails: false
+
+# List of thumbnails to precalculate when an image is uploaded.
+#
+#thumbnail_sizes:
+#  - width: 32
+#    height: 32
+#    method: crop
+#  - width: 96
+#    height: 96
+#    method: crop
+#  - width: 320
+#    height: 240
+#    method: scale
+#  - width: 640
+#    height: 480
+#    method: scale
+#  - width: 800
+#    height: 600
+#    method: scale
+
+# Is the preview URL API enabled?
+#
+# 'false' by default: uncomment the following to enable it (and specify a
+# url_preview_ip_range_blacklist blacklist).
+#
+url_preview_enabled: {{ matrix_synapse_url_preview_enabled|to_json }}
+
+# List of IP address CIDR ranges that the URL preview spider is denied
+# from accessing.  There are no defaults: you must explicitly
+# specify a list for URL previewing to work.  You should specify any
+# internal services in your network that you do not want synapse to try
+# to connect to, otherwise anyone in any Matrix room could cause your
+# synapse to issue arbitrary GET requests to your internal services,
+# causing serious security issues.
+#
+# (0.0.0.0 and :: are always blacklisted, whether or not they are explicitly
+# listed here, since they correspond to unroutable addresses.)
+#
+# This must be specified if url_preview_enabled is set. It is recommended that
+# you uncomment the following list as a starting point.
+#
+url_preview_ip_range_blacklist:
+  - '127.0.0.0/8'
+  - '10.0.0.0/8'
+  - '172.16.0.0/12'
+  - '192.168.0.0/16'
+  - '100.64.0.0/10'
+  - '192.0.0.0/24'
+  - '169.254.0.0/16'
+  - '192.88.99.0/24'
+  - '198.18.0.0/15'
+  - '192.0.2.0/24'
+  - '198.51.100.0/24'
+  - '203.0.113.0/24'
+  - '224.0.0.0/4'
+  - '::1/128'
+  - 'fe80::/10'
+  - 'fc00::/7'
+  - '2001:db8::/32'
+  - 'ff00::/8'
+  - 'fec0::/10'
+
+# List of IP address CIDR ranges that the URL preview spider is allowed
+# to access even if they are specified in url_preview_ip_range_blacklist.
+# This is useful for specifying exceptions to wide-ranging blacklisted
+# target IP ranges - e.g. for enabling URL previews for a specific private
+# website only visible in your network.
+#
+#url_preview_ip_range_whitelist:
+#   - '192.168.1.1'
+
+# Optional list of URL matches that the URL preview spider is
+# denied from accessing.  You should use url_preview_ip_range_blacklist
+# in preference to this, otherwise someone could define a public DNS
+# entry that points to a private IP address and circumvent the blacklist.
+# This is more useful if you know there is an entire shape of URL that
+# you know that will never want synapse to try to spider.
+#
+# Each list entry is a dictionary of url component attributes as returned
+# by urlparse.urlsplit as applied to the absolute form of the URL.  See
+# https://docs.python.org/2/library/urlparse.html#urlparse.urlsplit
+# The values of the dictionary are treated as an filename match pattern
+# applied to that component of URLs, unless they start with a ^ in which
+# case they are treated as a regular expression match.  If all the
+# specified component matches for a given list item succeed, the URL is
+# blacklisted.
+#
+#url_preview_url_blacklist:
+#  # blacklist any URL with a username in its URI
+#  - username: '*'
+#
+#  # blacklist all *.google.com URLs
+#  - netloc: 'google.com'
+#  - netloc: '*.google.com'
+#
+#  # blacklist all plain HTTP URLs
+#  - scheme: 'http'
+#
+#  # blacklist http(s)://www.acme.com/foo
+#  - netloc: 'www.acme.com'
+#    path: '/foo'
+#
+#  # blacklist any URL with a literal IPv4 address
+#  - netloc: '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
+
+# The largest allowed URL preview spidering size in bytes
+#
+max_spider_size: 10M
+
+# A list of values for the Accept-Language HTTP header used when
+# downloading webpages during URL preview generation. This allows
+# Synapse to specify the preferred languages that URL previews should
+# be in when communicating with remote servers.
+#
+# Each value is a IETF language tag; a 2-3 letter identifier for a
+# language, optionally followed by subtags separated by '-', specifying
+# a country or region variant.
+#
+# Multiple values can be provided, and a weight can be added to each by
+# using quality value syntax (;q=). '*' translates to any language.
+#
+# Defaults to "en".
+#
+# Example:
+#
+# url_preview_accept_language:
+#   - en-UK
+#   - en-US;q=0.9
+#   - fr;q=0.8
+#   - *;q=0.7
+#
+url_preview_accept_language:
+#   - en
+
+
+## Captcha ##
+# See docs/CAPTCHA_SETUP.md for full details of configuring this.
+
+# This homeserver's ReCAPTCHA public key. Must be specified if
+# enable_registration_captcha is enabled.
+#
+recaptcha_public_key: {{ matrix_synapse_recaptcha_public_key|to_json }}
+
+# This homeserver's ReCAPTCHA private key. Must be specified if
+# enable_registration_captcha is enabled.
+#
+recaptcha_private_key: {{ matrix_synapse_recaptcha_private_key|to_json }}
+
+# Uncomment to enable ReCaptcha checks when registering, preventing signup
+# unless a captcha is answered. Requires a valid ReCaptcha
+# public/private key. Defaults to 'false'.
+#
+enable_registration_captcha: {{ matrix_synapse_enable_registration_captcha|to_json }}
+
+# The API endpoint to use for verifying m.login.recaptcha responses.
+# Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify".
+#
+#recaptcha_siteverify_api: "https://my.recaptcha.site"
+
+
+## TURN ##
+
+# The public URIs of the TURN server to give to clients
+#
+turn_uris: {{ matrix_synapse_turn_uris|to_json }}
+
+# The shared secret used to compute passwords for the TURN server
+#
+turn_shared_secret: {{ matrix_synapse_turn_shared_secret|string|to_json }}
+
+# The Username and password if the TURN server needs them and
+# does not use a token
+#
+#turn_username: "TURNSERVER_USERNAME"
+#turn_password: "TURNSERVER_PASSWORD"
+
+# How long generated TURN credentials last
+#
+#turn_user_lifetime: 1h
+
+# Whether guests should be allowed to use the TURN server.
+# This defaults to True, otherwise VoIP will be unreliable for guests.
+# However, it does introduce a slight security risk as it allows users to
+# connect to arbitrary endpoints without having first signed up for a
+# valid account (e.g. by passing a CAPTCHA).
+#
+turn_allow_guests: {{ matrix_synapse_turn_allow_guests|to_json }}
+
+
+## Registration ##
+#
+# Registration can be rate-limited using the parameters in the "Ratelimiting"
+# section of this file.
+
+# Enable registration for new users.
+#
+enable_registration: {{ matrix_synapse_enable_registration|to_json }}
+
+# Time that a user's session remains valid for, after they log in.
+#
+# Note that this is not currently compatible with guest logins.
+#
+# Note also that this is calculated at login time: changes are not applied
+# retrospectively to users who have already logged in.
+#
+# By default, this is infinite.
+#
+#session_lifetime: 24h
+
+# The user must provide all of the below types of 3PID when registering.
+#
+#registrations_require_3pid:
+#  - email
+#  - msisdn
+{% if matrix_synapse_registrations_require_3pid|length > 0 %}
+registrations_require_3pid: {{ matrix_synapse_registrations_require_3pid|to_json }}
+{% endif %}
+
+# Explicitly disable asking for MSISDNs from the registration
+# flow (overrides registrations_require_3pid if MSISDNs are set as required)
+#
+#disable_msisdn_registration: true
+
+# Mandate that users are only allowed to associate certain formats of
+# 3PIDs with accounts on this server.
+#
+#allowed_local_3pids:
+#  - medium: email
+#    pattern: '^[^@]+@matrix\.org$'
+#  - medium: email
+#    pattern: '^[^@]+@vector\.im$'
+#  - medium: msisdn
+#    pattern: '\+44'
+{% if matrix_synapse_allowed_local_3pids|length > 0 %}
+allowed_local_3pids: {{ matrix_synapse_allowed_local_3pids|to_json }}
+{% endif %}
+
+# Enable 3PIDs lookup requests to identity servers from this server.
+#
+#enable_3pid_lookup: true
+
+# If set, allows registration of standard or admin accounts by anyone who
+# has the shared secret, even if registration is otherwise disabled.
+#
+registration_shared_secret: {{ matrix_synapse_registration_shared_secret|string|to_json }}
+
+# Set the number of bcrypt rounds used to generate password hash.
+# Larger numbers increase the work factor needed to generate the hash.
+# The default number is 12 (which equates to 2^12 rounds).
+# N.B. that increasing this will exponentially increase the time required
+# to register or login - e.g. 24 => 2^24 rounds which will take >20 mins.
+#
+#bcrypt_rounds: 12
+
+# Allows users to register as guests without a password/email/etc, and
+# participate in rooms hosted on this server which have been made
+# accessible to anonymous users.
+#
+allow_guest_access: {{ matrix_synapse_allow_guest_access|to_json }}
+
+# The identity server which we suggest that clients should use when users log
+# in on this server.
+#
+# (By default, no suggestion is made, so it is left up to the client.
+# This setting is ignored unless public_baseurl is also set.)
+#
+#default_identity_server: https://matrix.org
+
+# Handle threepid (email/phone etc) registration and password resets through a set of
+# *trusted* identity servers. Note that this allows the configured identity server to
+# reset passwords for accounts!
+#
+# Be aware that if `email` is not set, and SMTP options have not been
+# configured in the email config block, registration and user password resets via
+# email will be globally disabled.
+#
+# Additionally, if `msisdn` is not set, registration and password resets via msisdn
+# will be disabled regardless, and users will not be able to associate an msisdn
+# identifier to their account. This is due to Synapse currently not supporting
+# any method of sending SMS messages on its own.
+#
+# To enable using an identity server for operations regarding a particular third-party
+# identifier type, set the value to the URL of that identity server as shown in the
+# examples below.
+#
+# Servers handling the these requests must answer the `/requestToken` endpoints defined
+# by the Matrix Identity Service API specification:
+# https://matrix.org/docs/spec/identity_service/latest
+#
+# If a delegate is specified, the config option public_baseurl must also be filled out.
+#
+account_threepid_delegates:
+    email: {{ matrix_synapse_account_threepid_delegates_email|to_json }}
+    msisdn: {{ matrix_synapse_account_threepid_delegates_msisdn|to_json }}
+
+# Whether users are allowed to change their displayname after it has
+# been initially set. Useful when provisioning users based on the
+# contents of a third-party directory.
+#
+# Does not apply to server administrators. Defaults to 'true'
+#
+#enable_set_displayname: false
+
+# Whether users are allowed to change their avatar after it has been
+# initially set. Useful when provisioning users based on the contents
+# of a third-party directory.
+#
+# Does not apply to server administrators. Defaults to 'true'
+#
+#enable_set_avatar_url: false
+
+# Whether users can change the 3PIDs associated with their accounts
+# (email address and msisdn).
+#
+# Defaults to 'true'
+#
+#enable_3pid_changes: false
+
+# Users who register on this homeserver will automatically be joined
+# to these rooms.
+#
+# By default, any room aliases included in this list will be created
+# as a publicly joinable room when the first user registers for the
+# homeserver. This behaviour can be customised with the settings below.
+# If the room already exists, make certain it is a publicly joinable
+# room. The join rule of the room must be set to 'public'.
+#
+#auto_join_rooms:
+#  - "#example:example.com"
+{% if matrix_synapse_auto_join_rooms|length > 0 %}
+auto_join_rooms:
+{{ matrix_synapse_auto_join_rooms|to_nice_yaml }}
+{% endif %}
+
+# Where auto_join_rooms are specified, setting this flag ensures that the
+# the rooms exist by creating them when the first user on the
+# homeserver registers.
+#
+# By default the auto-created rooms are publicly joinable from any federated
+# server. Use the autocreate_auto_join_rooms_federated and
+# autocreate_auto_join_room_preset settings below to customise this behaviour.
+#
+# Setting to false means that if the rooms are not manually created,
+# users cannot be auto-joined since they do not exist.
+#
+# Defaults to true. Uncomment the following line to disable automatically
+# creating auto-join rooms.
+#
+autocreate_auto_join_rooms: {{ matrix_synapse_autocreate_auto_join_rooms|to_json }}
+
+# Whether the auto_join_rooms that are auto-created are available via
+# federation. Only has an effect if autocreate_auto_join_rooms is true.
+#
+# Note that whether a room is federated cannot be modified after
+# creation.
+#
+# Defaults to true: the room will be joinable from other servers.
+# Uncomment the following to prevent users from other homeservers from
+# joining these rooms.
+#
+#autocreate_auto_join_rooms_federated: false
+
+# The room preset to use when auto-creating one of auto_join_rooms. Only has an
+# effect if autocreate_auto_join_rooms is true.
+#
+# This can be one of "public_chat", "private_chat", or "trusted_private_chat".
+# If a value of "private_chat" or "trusted_private_chat" is used then
+# auto_join_mxid_localpart must also be configured.
+#
+# Defaults to "public_chat", meaning that the room is joinable by anyone, including
+# federated servers if autocreate_auto_join_rooms_federated is true (the default).
+# Uncomment the following to require an invitation to join these rooms.
+#
+#autocreate_auto_join_room_preset: private_chat
+
+# The local part of the user id which is used to create auto_join_rooms if
+# autocreate_auto_join_rooms is true. If this is not provided then the
+# initial user account that registers will be used to create the rooms.
+#
+# The user id is also used to invite new users to any auto-join rooms which
+# are set to invite-only.
+#
+# It *must* be configured if autocreate_auto_join_room_preset is set to
+# "private_chat" or "trusted_private_chat".
+#
+# Note that this must be specified in order for new users to be correctly
+# invited to any auto-join rooms which have been set to invite-only (either
+# at the time of creation or subsequently).
+#
+# Note that, if the room already exists, this user must be joined and
+# have the appropriate permissions to invite new members.
+#
+#auto_join_mxid_localpart: system
+
+# When auto_join_rooms is specified, setting this flag to false prevents
+# guest accounts from being automatically joined to the rooms.
+#
+# Defaults to true.
+#
+#auto_join_rooms_for_guests: false
+
+
+## Account Validity ##
+
+# Optional account validity configuration. This allows for accounts to be denied
+# any request after a given period.
+#
+# Once this feature is enabled, Synapse will look for registered users without an
+# expiration date at startup and will add one to every account it found using the
+# current settings at that time.
+# This means that, if a validity period is set, and Synapse is restarted (it will
+# then derive an expiration date from the current validity period), and some time
+# after that the validity period changes and Synapse is restarted, the users'
+# expiration dates won't be updated unless their account is manually renewed. This
+# date will be randomly selected within a range [now + period - d ; now + period],
+# where d is equal to 10% of the validity period.
+#
+account_validity:
+  # The account validity feature is disabled by default. Uncomment the
+  # following line to enable it.
+  #
+  #enabled: true
+
+  # The period after which an account is valid after its registration. When
+  # renewing the account, its validity period will be extended by this amount
+  # of time. This parameter is required when using the account validity
+  # feature.
+  #
+  #period: 6w
+
+  # The amount of time before an account's expiry date at which Synapse will
+  # send an email to the account's email address with a renewal link. By
+  # default, no such emails are sent.
+  #
+  # If you enable this setting, you will also need to fill out the 'email' and
+  # 'public_baseurl' configuration sections.
+  #
+  #renew_at: 1w
+
+  # The subject of the email sent out with the renewal link. '%(app)s' can be
+  # used as a placeholder for the 'app_name' parameter from the 'email'
+  # section.
+  #
+  # Note that the placeholder must be written '%(app)s', including the
+  # trailing 's'.
+  #
+  # If this is not set, a default value is used.
+  #
+  #renew_email_subject: "Renew your %(app)s account"
+
+  # Directory in which Synapse will try to find templates for the HTML files to
+  # serve to the user when trying to renew an account. If not set, default
+  # templates from within the Synapse package will be used.
+  #
+  # The currently available templates are:
+  #
+  # * account_renewed.html: Displayed to the user after they have successfully
+  #       renewed their account.
+  #
+  # * account_previously_renewed.html: Displayed to the user if they attempt to
+  #       renew their account with a token that is valid, but that has already
+  #       been used. In this case the account is not renewed again.
+  #
+  # * invalid_token.html: Displayed to the user when they try to renew an account
+  #       with an unknown or invalid renewal token.
+  #
+  # See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for
+  # default template contents.
+  #
+  # The file name of some of these templates can be configured below for legacy
+  # reasons.
+  #
+  #template_dir: "res/templates"
+
+  # A custom file name for the 'account_renewed.html' template.
+  #
+  # If not set, the file is assumed to be named "account_renewed.html".
+  #
+  #account_renewed_html_path: "account_renewed.html"
+
+  # A custom file name for the 'invalid_token.html' template.
+  #
+  # If not set, the file is assumed to be named "invalid_token.html".
+  #
+  #invalid_token_html_path: "invalid_token.html"
+
+
+## Metrics ###
+
+# Enable collection and rendering of performance metrics
+#
+enable_metrics: {{ matrix_synapse_metrics_enabled|to_json  }}
+
+# Enable sentry integration
+# NOTE: While attempts are made to ensure that the logs don't contain
+# any sensitive information, this cannot be guaranteed. By enabling
+# this option the sentry server may therefore receive sensitive
+# information, and it in turn may then diseminate sensitive information
+# through insecure notification channels if so configured.
+#
+{% if matrix_synapse_sentry_dsn != "" %}
+sentry:
+    dsn: {{ matrix_synapse_sentry_dsn|to_json }}
+{% endif %}
+
+# Flags to enable Prometheus metrics which are not suitable to be
+# enabled by default, either for performance reasons or limited use.
+#
+metrics_flags:
+    # Publish synapse_federation_known_servers, a gauge of the number of
+    # servers this homeserver knows about, including itself. May cause
+    # performance problems on large homeservers.
+    #
+    #known_servers: true
+
+# Whether or not to report anonymized homeserver usage statistics.
+#
+report_stats: {{ matrix_synapse_report_stats|to_json }}
+
+# The endpoint to report the anonymized homeserver usage statistics to.
+# Defaults to https://matrix.org/report-usage-stats/push
+#
+#report_stats_endpoint: https://example.com/report-usage-stats/push
+
+
+## API Configuration ##
+
+# Controls for the state that is shared with users who receive an invite
+# to a room
+#
+room_prejoin_state:
+   # By default, the following state event types are shared with users who
+   # receive invites to the room:
+   #
+   # - m.room.join_rules
+   # - m.room.canonical_alias
+   # - m.room.avatar
+   # - m.room.encryption
+   # - m.room.name
+   # - m.room.create
+   #
+   # Uncomment the following to disable these defaults (so that only the event
+   # types listed in 'additional_event_types' are shared). Defaults to 'false'.
+   #
+   #disable_default_event_types: true
+
+   # Additional state event types to share with users when they are invited
+   # to a room.
+   #
+   # By default, this list is empty (so only the default event types are shared).
+   #
+   #additional_event_types:
+   #  - org.example.custom.event.type
+
+
+# A list of application service config files to use
+#
+app_service_config_files: {{ matrix_synapse_app_service_config_files|to_json }}
+
+# Uncomment to enable tracking of application service IP addresses. Implicitly
+# enables MAU tracking for application service users.
+#
+#track_appservice_user_ips: True
+
+
+# a secret which is used to sign access tokens. If none is specified,
+# the registration_shared_secret is used, if one is given; otherwise,
+# a secret key is derived from the signing key.
+#
+macaroon_secret_key: {{ matrix_synapse_macaroon_secret_key|string|to_json }}
+
+# a secret which is used to calculate HMACs for form values, to stop
+# falsification of values. Must be specified for the User Consent
+# forms to work.
+#
+form_secret: {{ matrix_synapse_form_secret|string|to_json }}
+
+## Signing Keys ##
+
+# Path to the signing key to sign messages with
+#
+signing_key_path: "/data/{{ matrix_server_fqn_matrix }}.signing.key"
+
+# The keys that the server used to sign messages with but won't use
+# to sign new messages.
+#
+old_signing_keys:
+  # For each key, `key` should be the base64-encoded public key, and
+  # `expired_ts`should be the time (in milliseconds since the unix epoch) that
+  # it was last used.
+  #
+  # It is possible to build an entry from an old signing.key file using the
+  # `export_signing_key` script which is provided with synapse.
+  #
+  # For example:
+  #
+  #"ed25519:id": { key: "base64string", expired_ts: 123456789123 }
+
+# How long key response published by this server is valid for.
+# Used to set the valid_until_ts in /key/v2 APIs.
+# Determines how quickly servers will query to check which keys
+# are still valid.
+#
+#key_refresh_interval: 1d
+
+# The trusted servers to download signing keys from.
+#
+# When we need to fetch a signing key, each server is tried in parallel.
+#
+# Normally, the connection to the key server is validated via TLS certificates.
+# Additional security can be provided by configuring a `verify key`, which
+# will make synapse check that the response is signed by that key.
+#
+# This setting supercedes an older setting named `perspectives`. The old format
+# is still supported for backwards-compatibility, but it is deprecated.
+#
+# 'trusted_key_servers' defaults to matrix.org, but using it will generate a
+# warning on start-up. To suppress this warning, set
+# 'suppress_key_server_warning' to true.
+#
+# Options for each entry in the list include:
+#
+#    server_name: the name of the server. required.
+#
+#    verify_keys: an optional map from key id to base64-encoded public key.
+#       If specified, we will check that the response is signed by at least
+#       one of the given keys.
+#
+#    accept_keys_insecurely: a boolean. Normally, if `verify_keys` is unset,
+#       and federation_verify_certificates is not `true`, synapse will refuse
+#       to start, because this would allow anyone who can spoof DNS responses
+#       to masquerade as the trusted key server. If you know what you are doing
+#       and are sure that your network environment provides a secure connection
+#       to the key server, you can set this to `true` to override this
+#       behaviour.
+#
+# An example configuration might look like:
+#
+#trusted_key_servers:
+#  - server_name: "my_trusted_server.example.com"
+#    verify_keys:
+#      "ed25519:auto": "abcdefghijklmnopqrstuvwxyzabcdefghijklmopqr"
+#  - server_name: "my_other_trusted_server.example.com"
+#
+trusted_key_servers: {{ matrix_synapse_trusted_key_servers|to_json }}
+
+
+# Uncomment the following to disable the warning that is emitted when the
+# trusted_key_servers include 'matrix.org'. See above.
+#
+#suppress_key_server_warning: true
+
+# The signing keys to use when acting as a trusted key server. If not specified
+# defaults to the server signing key.
+#
+# Can contain multiple keys, one per line.
+#
+#key_server_signing_keys_path: "key_server_signing_keys.key"
+
+
+## Single sign-on integration ##
+
+# The following settings can be used to make Synapse use a single sign-on
+# provider for authentication, instead of its internal password database.
+#
+# You will probably also want to set the following options to `false` to
+# disable the regular login/registration flows:
+#   * enable_registration
+#   * password_config.enabled
+#
+# You will also want to investigate the settings under the "sso" configuration
+# section below.
+
+# Enable SAML2 for registration and login. Uses pysaml2.
+#
+# At least one of `sp_config` or `config_path` must be set in this section to
+# enable SAML login.
+#
+# Once SAML support is enabled, a metadata file will be exposed at
+# https://<server>:<port>/_synapse/client/saml2/metadata.xml, which you may be able to
+# use to configure your SAML IdP with. Alternatively, you can manually configure
+# the IdP to use an ACS location of
+# https://<server>:<port>/_synapse/client/saml2/authn_response.
+#
+saml2_config:
+  # `sp_config` is the configuration for the pysaml2 Service Provider.
+  # See pysaml2 docs for format of config.
+  #
+  # Default values will be used for the 'entityid' and 'service' settings,
+  # so it is not normally necessary to specify them unless you need to
+  # override them.
+  #
+  sp_config:
+    # Point this to the IdP's metadata. You must provide either a local
+    # file via the `local` attribute or (preferably) a URL via the
+    # `remote` attribute.
+    #
+    #metadata:
+    #  local: ["saml2/idp.xml"]
+    #  remote:
+    #    - url: https://our_idp/metadata.xml
+
+    # Allowed clock difference in seconds between the homeserver and IdP.
+    #
+    # Uncomment the below to increase the accepted time difference from 0 to 3 seconds.
+    #
+    #accepted_time_diff: 3
+
+    # By default, the user has to go to our login page first. If you'd like
+    # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
+    # 'service.sp' section:
+    #
+    #service:
+    #  sp:
+    #    allow_unsolicited: true
+
+    # The examples below are just used to generate our metadata xml, and you
+    # may well not need them, depending on your setup. Alternatively you
+    # may need a whole lot more detail - see the pysaml2 docs!
+
+    #description: ["My awesome SP", "en"]
+    #name: ["Test SP", "en"]
+
+    #ui_info:
+    #  display_name:
+    #    - lang: en
+    #      text: "Display Name is the descriptive name of your service."
+    #  description:
+    #    - lang: en
+    #      text: "Description should be a short paragraph explaining the purpose of the service."
+    #  information_url:
+    #    - lang: en
+    #      text: "https://example.com/terms-of-service"
+    #  privacy_statement_url:
+    #    - lang: en
+    #      text: "https://example.com/privacy-policy"
+    #  keywords:
+    #    - lang: en
+    #      text: ["Matrix", "Element"]
+    #  logo:
+    #    - lang: en
+    #      text: "https://example.com/logo.svg"
+    #      width: "200"
+    #      height: "80"
+
+    #organization:
+    #  name: Example com
+    #  display_name:
+    #    - ["Example co", "en"]
+    #  url: "http://example.com"
+
+    #contact_person:
+    #  - given_name: Bob
+    #    sur_name: "the Sysadmin"
+    #    email_address": ["admin@example.com"]
+    #    contact_type": technical
+
+  # Instead of putting the config inline as above, you can specify a
+  # separate pysaml2 configuration file:
+  #
+  #config_path: "/data/sp_conf.py"
+
+  # The lifetime of a SAML session. This defines how long a user has to
+  # complete the authentication process, if allow_unsolicited is unset.
+  # The default is 15 minutes.
+  #
+  #saml_session_lifetime: 5m
+
+  # An external module can be provided here as a custom solution to
+  # mapping attributes returned from a saml provider onto a matrix user.
+  #
+  user_mapping_provider:
+    # The custom module's class. Uncomment to use a custom module.
+    #
+    #module: mapping_provider.SamlMappingProvider
+
+    # Custom configuration values for the module. Below options are
+    # intended for the built-in provider, they should be changed if
+    # using a custom module. This section will be passed as a Python
+    # dictionary to the module's `parse_config` method.
+    #
+    config:
+      # The SAML attribute (after mapping via the attribute maps) to use
+      # to derive the Matrix ID from. 'uid' by default.
+      #
+      # Note: This used to be configured by the
+      # saml2_config.mxid_source_attribute option. If that is still
+      # defined, its value will be used instead.
+      #
+      #mxid_source_attribute: displayName
+
+      # The mapping system to use for mapping the saml attribute onto a
+      # matrix ID.
+      #
+      # Options include:
+      #  * 'hexencode' (which maps unpermitted characters to '=xx')
+      #  * 'dotreplace' (which replaces unpermitted characters with
+      #     '.').
+      # The default is 'hexencode'.
+      #
+      # Note: This used to be configured by the
+      # saml2_config.mxid_mapping option. If that is still defined, its
+      # value will be used instead.
+      #
+      #mxid_mapping: dotreplace
+
+  # In previous versions of synapse, the mapping from SAML attribute to
+  # MXID was always calculated dynamically rather than stored in a
+  # table. For backwards- compatibility, we will look for user_ids
+  # matching such a pattern before creating a new account.
+  #
+  # This setting controls the SAML attribute which will be used for this
+  # backwards-compatibility lookup. Typically it should be 'uid', but if
+  # the attribute maps are changed, it may be necessary to change it.
+  #
+  # The default is 'uid'.
+  #
+  #grandfathered_mxid_source_attribute: upn
+
+  # It is possible to configure Synapse to only allow logins if SAML attributes
+  # match particular values. The requirements can be listed under
+  # `attribute_requirements` as shown below. All of the listed attributes must
+  # match for the login to be permitted.
+  #
+  #attribute_requirements:
+  #  - attribute: userGroup
+  #    value: "staff"
+  #  - attribute: department
+  #    value: "sales"
+
+  # If the metadata XML contains multiple IdP entities then the `idp_entityid`
+  # option must be set to the entity to redirect users to.
+  #
+  # Most deployments only have a single IdP entity and so should omit this
+  # option.
+  #
+  #idp_entityid: 'https://our_idp/entityid'
+
+
+# List of OpenID Connect (OIDC) / OAuth 2.0 identity providers, for registration
+# and login.
+#
+# Options for each entry include:
+#
+#   idp_id: a unique identifier for this identity provider. Used internally
+#       by Synapse; should be a single word such as 'github'.
+#
+#       Note that, if this is changed, users authenticating via that provider
+#       will no longer be recognised as the same user!
+#
+#       (Use "oidc" here if you are migrating from an old "oidc_config"
+#       configuration.)
+#
+#   idp_name: A user-facing name for this identity provider, which is used to
+#       offer the user a choice of login mechanisms.
+#
+#   idp_icon: An optional icon for this identity provider, which is presented
+#       by clients and Synapse's own IdP picker page. If given, must be an
+#       MXC URI of the format mxc://<server-name>/<media-id>. (An easy way to
+#       obtain such an MXC URI is to upload an image to an (unencrypted) room
+#       and then copy the "url" from the source of the event.)
+#
+#   idp_brand: An optional brand for this identity provider, allowing clients
+#       to style the login flow according to the identity provider in question.
+#       See the spec for possible options here.
+#
+#   discover: set to 'false' to disable the use of the OIDC discovery mechanism
+#       to discover endpoints. Defaults to true.
+#
+#   issuer: Required. The OIDC issuer. Used to validate tokens and (if discovery
+#       is enabled) to discover the provider's endpoints.
+#
+#   client_id: Required. oauth2 client id to use.
+#
+#   client_secret: oauth2 client secret to use. May be omitted if
+#        client_secret_jwt_key is given, or if client_auth_method is 'none'.
+#
+#   client_secret_jwt_key: Alternative to client_secret: details of a key used
+#      to create a JSON Web Token to be used as an OAuth2 client secret. If
+#      given, must be a dictionary with the following properties:
+#
+#          key: a pem-encoded signing key. Must be a suitable key for the
+#              algorithm specified. Required unless 'key_file' is given.
+#
+#          key_file: the path to file containing a pem-encoded signing key file.
+#              Required unless 'key' is given.
+#
+#          jwt_header: a dictionary giving properties to include in the JWT
+#              header. Must include the key 'alg', giving the algorithm used to
+#              sign the JWT, such as "ES256", using the JWA identifiers in
+#              RFC7518.
+#
+#          jwt_payload: an optional dictionary giving properties to include in
+#              the JWT payload. Normally this should include an 'iss' key.
+#
+#   client_auth_method: auth method to use when exchanging the token. Valid
+#       values are 'client_secret_basic' (default), 'client_secret_post' and
+#       'none'.
+#
+#   scopes: list of scopes to request. This should normally include the "openid"
+#       scope. Defaults to ["openid"].
+#
+#   authorization_endpoint: the oauth2 authorization endpoint. Required if
+#       provider discovery is disabled.
+#
+#   token_endpoint: the oauth2 token endpoint. Required if provider discovery is
+#       disabled.
+#
+#   userinfo_endpoint: the OIDC userinfo endpoint. Required if discovery is
+#       disabled and the 'openid' scope is not requested.
+#
+#   jwks_uri: URI where to fetch the JWKS. Required if discovery is disabled and
+#       the 'openid' scope is used.
+#
+#   skip_verification: set to 'true' to skip metadata verification. Use this if
+#       you are connecting to a provider that is not OpenID Connect compliant.
+#       Defaults to false. Avoid this in production.
+#
+#   user_profile_method: Whether to fetch the user profile from the userinfo
+#       endpoint. Valid values are: 'auto' or 'userinfo_endpoint'.
+#
+#       Defaults to 'auto', which fetches the userinfo endpoint if 'openid' is
+#       included in 'scopes'. Set to 'userinfo_endpoint' to always fetch the
+#       userinfo endpoint.
+#
+#   allow_existing_users: set to 'true' to allow a user logging in via OIDC to
+#       match a pre-existing account instead of failing. This could be used if
+#       switching from password logins to OIDC. Defaults to false.
+#
+#   user_mapping_provider: Configuration for how attributes returned from a OIDC
+#       provider are mapped onto a matrix user. This setting has the following
+#       sub-properties:
+#
+#       module: The class name of a custom mapping module. Default is
+#           'synapse.handlers.oidc.JinjaOidcMappingProvider'.
+#           See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers
+#           for information on implementing a custom mapping provider.
+#
+#       config: Configuration for the mapping provider module. This section will
+#           be passed as a Python dictionary to the user mapping provider
+#           module's `parse_config` method.
+#
+#           For the default provider, the following settings are available:
+#
+#             subject_claim: name of the claim containing a unique identifier
+#                 for the user. Defaults to 'sub', which OpenID Connect
+#                 compliant providers should provide.
+#
+#             localpart_template: Jinja2 template for the localpart of the MXID.
+#                 If this is not set, the user will be prompted to choose their
+#                 own username (see 'sso_auth_account_details.html' in the 'sso'
+#                 section of this file).
+#
+#             display_name_template: Jinja2 template for the display name to set
+#                 on first login. If unset, no displayname will be set.
+#
+#             email_template: Jinja2 template for the email address of the user.
+#                 If unset, no email address will be added to the account.
+#
+#             extra_attributes: a map of Jinja2 templates for extra attributes
+#                 to send back to the client during login.
+#                 Note that these are non-standard and clients will ignore them
+#                 without modifications.
+#
+#           When rendering, the Jinja2 templates are given a 'user' variable,
+#           which is set to the claims returned by the UserInfo Endpoint and/or
+#           in the ID Token.
+#
+#   It is possible to configure Synapse to only allow logins if certain attributes
+#   match particular values in the OIDC userinfo. The requirements can be listed under
+#   `attribute_requirements` as shown below. All of the listed attributes must
+#   match for the login to be permitted. Additional attributes can be added to
+#   userinfo by expanding the `scopes` section of the OIDC config to retrieve
+#   additional information from the OIDC provider.
+#
+#   If the OIDC claim is a list, then the attribute must match any value in the list.
+#   Otherwise, it must exactly match the value of the claim. Using the example
+#   below, the `family_name` claim MUST be "Stephensson", but the `groups`
+#   claim MUST contain "admin".
+#
+#   attribute_requirements:
+#     - attribute: family_name
+#       value: "Stephensson"
+#     - attribute: groups
+#       value: "admin"
+#
+# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md
+# for information on how to configure these options.
+#
+# For backwards compatibility, it is also possible to configure a single OIDC
+# provider via an 'oidc_config' setting. This is now deprecated and admins are
+# advised to migrate to the 'oidc_providers' format. (When doing that migration,
+# use 'oidc' for the idp_id to ensure that existing users continue to be
+# recognised.)
+#
+oidc_providers:
+  # Generic example
+  #
+  #- idp_id: my_idp
+  #  idp_name: "My OpenID provider"
+  #  idp_icon: "mxc://example.com/mediaid"
+  #  discover: false
+  #  issuer: "https://accounts.example.com/"
+  #  client_id: "provided-by-your-issuer"
+  #  client_secret: "provided-by-your-issuer"
+  #  client_auth_method: client_secret_post
+  #  scopes: ["openid", "profile"]
+  #  authorization_endpoint: "https://accounts.example.com/oauth2/auth"
+  #  token_endpoint: "https://accounts.example.com/oauth2/token"
+  #  userinfo_endpoint: "https://accounts.example.com/userinfo"
+  #  jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
+  #  user_mapping_provider:
+  #    config:
+  #      subject_claim: "id"
+  #      localpart_template: "{% raw %}{{ user.login }}{% endraw %}"
+  #      display_name_template: "{% raw %}{{ user.name }}{% endraw %}"
+  #      email_template: "{% raw %}{{ user.email }}{% endraw %}"
+  #  attribute_requirements:
+  #    - attribute: userGroup
+  #      value: "synapseUsers"
+
+
+# Enable Central Authentication Service (CAS) for registration and login.
+#
+cas_config:
+  # Uncomment the following to enable authorization against a CAS server.
+  # Defaults to false.
+  #
+  #enabled: true
+
+  # The URL of the CAS authorization endpoint.
+  #
+  #server_url: "https://cas-server.com"
+
+  # The attribute of the CAS response to use as the display name.
+  #
+  # If unset, no displayname will be set.
+  #
+  #displayname_attribute: name
+
+  # It is possible to configure Synapse to only allow logins if CAS attributes
+  # match particular values. All of the keys in the mapping below must exist
+  # and the values must match the given value. Alternately if the given value
+  # is None then any value is allowed (the attribute just must exist).
+  # All of the listed attributes must match for the login to be permitted.
+  #
+  #required_attributes:
+  #  userGroup: "staff"
+  #  department: None
+
+
+# Additional settings to use with single-sign on systems such as OpenID Connect,
+# SAML2 and CAS.
+#
+sso:
+    # A list of client URLs which are whitelisted so that the user does not
+    # have to confirm giving access to their account to the URL. Any client
+    # whose URL starts with an entry in the following list will not be subject
+    # to an additional confirmation step after the SSO login is completed.
+    #
+    # WARNING: An entry such as "https://my.client" is insecure, because it
+    # will also match "https://my.client.evil.site", exposing your users to
+    # phishing attacks from evil.site. To avoid this, include a slash after the
+    # hostname: "https://my.client/".
+    #
+    # If public_baseurl is set, then the login fallback page (used by clients
+    # that don't natively support the required login flows) is whitelisted in
+    # addition to any URLs in this list.
+    #
+    # By default, this list is empty.
+    #
+    #client_whitelist:
+    #  - https://riot.im/develop
+    #  - https://my.custom.client/
+
+    # Uncomment to keep a user's profile fields in sync with information from
+    # the identity provider. Currently only syncing the displayname is
+    # supported. Fields are checked on every SSO login, and are updated
+    # if necessary.
+    #
+    # Note that enabling this option will override user profile information,
+    # regardless of whether users have opted-out of syncing that
+    # information when first signing in. Defaults to false.
+    #
+    #update_profile_information: true
+
+    # Directory in which Synapse will try to find the template files below.
+    # If not set, or the files named below are not found within the template
+    # directory, default templates from within the Synapse package will be used.
+    #
+    # Synapse will look for the following templates in this directory:
+    #
+    # * HTML page to prompt the user to choose an Identity Provider during
+    #   login: 'sso_login_idp_picker.html'.
+    #
+    #   This is only used if multiple SSO Identity Providers are configured.
+    #
+    #   When rendering, this template is given the following variables:
+    #     * redirect_url: the URL that the user will be redirected to after
+    #       login.
+    #
+    #     * server_name: the homeserver's name.
+    #
+    #     * providers: a list of available Identity Providers. Each element is
+    #       an object with the following attributes:
+    #
+    #         * idp_id: unique identifier for the IdP
+    #         * idp_name: user-facing name for the IdP
+    #         * idp_icon: if specified in the IdP config, an MXC URI for an icon
+    #              for the IdP
+    #         * idp_brand: if specified in the IdP config, a textual identifier
+    #              for the brand of the IdP
+    #
+    #   The rendered HTML page should contain a form which submits its results
+    #   back as a GET request, with the following query parameters:
+    #
+    #     * redirectUrl: the client redirect URI (ie, the `redirect_url` passed
+    #       to the template)
+    #
+    #     * idp: the 'idp_id' of the chosen IDP.
+    #
+    # * HTML page to prompt new users to enter a userid and confirm other
+    #   details: 'sso_auth_account_details.html'. This is only shown if the
+    #   SSO implementation (with any user_mapping_provider) does not return
+    #   a localpart.
+    #
+    #   When rendering, this template is given the following variables:
+    #
+    #     * server_name: the homeserver's name.
+    #
+    #     * idp: details of the SSO Identity Provider that the user logged in
+    #       with: an object with the following attributes:
+    #
+    #         * idp_id: unique identifier for the IdP
+    #         * idp_name: user-facing name for the IdP
+    #         * idp_icon: if specified in the IdP config, an MXC URI for an icon
+    #              for the IdP
+    #         * idp_brand: if specified in the IdP config, a textual identifier
+    #              for the brand of the IdP
+    #
+    #     * user_attributes: an object containing details about the user that
+    #       we received from the IdP. May have the following attributes:
+    #
+    #         * display_name: the user's display_name
+    #         * emails: a list of email addresses
+    #
+    #   The template should render a form which submits the following fields:
+    #
+    #     * username: the localpart of the user's chosen user id
+    #
+    # * HTML page allowing the user to consent to the server's terms and
+    #   conditions. This is only shown for new users, and only if
+    #   `user_consent.require_at_registration` is set.
+    #
+    #   When rendering, this template is given the following variables:
+    #
+    #     * server_name: the homeserver's name.
+    #
+    #     * user_id: the user's matrix proposed ID.
+    #
+    #     * user_profile.display_name: the user's proposed display name, if any.
+    #
+    #     * consent_version: the version of the terms that the user will be
+    #       shown
+    #
+    #     * terms_url: a link to the page showing the terms.
+    #
+    #   The template should render a form which submits the following fields:
+    #
+    #     * accepted_version: the version of the terms accepted by the user
+    #       (ie, 'consent_version' from the input variables).
+    #
+    # * HTML page for a confirmation step before redirecting back to the client
+    #   with the login token: 'sso_redirect_confirm.html'.
+    #
+    #   When rendering, this template is given the following variables:
+    #
+    #     * redirect_url: the URL the user is about to be redirected to.
+    #
+    #     * display_url: the same as `redirect_url`, but with the query
+    #                    parameters stripped. The intention is to have a
+    #                    human-readable URL to show to users, not to use it as
+    #                    the final address to redirect to.
+    #
+    #     * server_name: the homeserver's name.
+    #
+    #     * new_user: a boolean indicating whether this is the user's first time
+    #          logging in.
+    #
+    #     * user_id: the user's matrix ID.
+    #
+    #     * user_profile.avatar_url: an MXC URI for the user's avatar, if any.
+    #           None if the user has not set an avatar.
+    #
+    #     * user_profile.display_name: the user's display name. None if the user
+    #           has not set a display name.
+    #
+    # * HTML page which notifies the user that they are authenticating to confirm
+    #   an operation on their account during the user interactive authentication
+    #   process: 'sso_auth_confirm.html'.
+    #
+    #   When rendering, this template is given the following variables:
+    #     * redirect_url: the URL the user is about to be redirected to.
+    #
+    #     * description: the operation which the user is being asked to confirm
+    #
+    #     * idp: details of the Identity Provider that we will use to confirm
+    #       the user's identity: an object with the following attributes:
+    #
+    #         * idp_id: unique identifier for the IdP
+    #         * idp_name: user-facing name for the IdP
+    #         * idp_icon: if specified in the IdP config, an MXC URI for an icon
+    #              for the IdP
+    #         * idp_brand: if specified in the IdP config, a textual identifier
+    #              for the brand of the IdP
+    #
+    # * HTML page shown after a successful user interactive authentication session:
+    #   'sso_auth_success.html'.
+    #
+    #   Note that this page must include the JavaScript which notifies of a successful authentication
+    #   (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback).
+    #
+    #   This template has no additional variables.
+    #
+    # * HTML page shown after a user-interactive authentication session which
+    #   does not map correctly onto the expected user: 'sso_auth_bad_user.html'.
+    #
+    #   When rendering, this template is given the following variables:
+    #     * server_name: the homeserver's name.
+    #     * user_id_to_verify: the MXID of the user that we are trying to
+    #       validate.
+    #
+    # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
+    #   attempts to login: 'sso_account_deactivated.html'.
+    #
+    #   This template has no additional variables.
+    #
+    # * HTML page to display to users if something goes wrong during the
+    #   OpenID Connect authentication process: 'sso_error.html'.
+    #
+    #   When rendering, this template is given two variables:
+    #     * error: the technical name of the error
+    #     * error_description: a human-readable message for the error
+    #
+    # You can see the default templates at:
+    # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
+    #
+    #template_dir: "res/templates"
+
+
+# JSON web token integration. The following settings can be used to make
+# Synapse JSON web tokens for authentication, instead of its internal
+# password database.
+#
+# Each JSON Web Token needs to contain a "sub" (subject) claim, which is
+# used as the localpart of the mxid.
+#
+# Additionally, the expiration time ("exp"), not before time ("nbf"),
+# and issued at ("iat") claims are validated if present.
+#
+# Note that this is a non-standard login type and client support is
+# expected to be non-existent.
+#
+# See https://github.com/matrix-org/synapse/blob/master/docs/jwt.md.
+#
+#jwt_config:
+    # Uncomment the following to enable authorization using JSON web
+    # tokens. Defaults to false.
+    #
+    #enabled: true
+
+    # This is either the private shared secret or the public key used to
+    # decode the contents of the JSON web token.
+    #
+    # Required if 'enabled' is true.
+    #
+    #secret: "provided-by-your-issuer"
+
+    # The algorithm used to sign the JSON web token.
+    #
+    # Supported algorithms are listed at
+    # https://pyjwt.readthedocs.io/en/latest/algorithms.html
+    #
+    # Required if 'enabled' is true.
+    #
+    #algorithm: "provided-by-your-issuer"
+
+    # The issuer to validate the "iss" claim against.
+    #
+    # Optional, if provided the "iss" claim will be required and
+    # validated for all JSON web tokens.
+    #
+    #issuer: "provided-by-your-issuer"
+
+    # A list of audiences to validate the "aud" claim against.
+    #
+    # Optional, if provided the "aud" claim will be required and
+    # validated for all JSON web tokens.
+    #
+    # Note that if the "aud" claim is included in a JSON web token then
+    # validation will fail without configuring audiences.
+    #
+    #audiences:
+    #    - "provided-by-your-issuer"
+
+
+password_config:
+   # Uncomment to disable password login
+   #
+   #enabled: false
+
+   # Uncomment to disable authentication against the local password
+   # database. This is ignored if `enabled` is false, and is only useful
+   # if you have other password_providers.
+   #
+   localdb_enabled: {{ matrix_synapse_password_config_localdb_enabled|to_json }}
+
+   # Uncomment and change to a secret random string for extra security.
+   # DO NOT CHANGE THIS AFTER INITIAL SETUP!
+   #
+   pepper: {{ matrix_synapse_password_config_pepper|string|to_json }}
+
+   # Define and enforce a password policy. Each parameter is optional.
+   # This is an implementation of MSC2000.
+   #
+   policy:
+      # Whether to enforce the password policy.
+      # Defaults to 'false'.
+      #
+      #enabled: true
+
+      # Minimum accepted length for a password.
+      # Defaults to 0.
+      #
+      #minimum_length: 15
+
+      # Whether a password must contain at least one digit.
+      # Defaults to 'false'.
+      #
+      #require_digit: true
+
+      # Whether a password must contain at least one symbol.
+      # A symbol is any character that's not a number or a letter.
+      # Defaults to 'false'.
+      #
+      #require_symbol: true
+
+      # Whether a password must contain at least one lowercase letter.
+      # Defaults to 'false'.
+      #
+      #require_lowercase: true
+
+      # Whether a password must contain at least one lowercase letter.
+      # Defaults to 'false'.
+      #
+      #require_uppercase: true
+
+ui_auth:
+    # The amount of time to allow a user-interactive authentication session
+    # to be active.
+    #
+    # This defaults to 0, meaning the user is queried for their credentials
+    # before every action, but this can be overridden to allow a single
+    # validation to be re-used.  This weakens the protections afforded by
+    # the user-interactive authentication process, by allowing for multiple
+    # (and potentially different) operations to use the same validation session.
+    #
+    # This is ignored for potentially "dangerous" operations (including
+    # deactivating an account, modifying an account password, and
+    # adding a 3PID).
+    #
+    # Uncomment below to allow for credential validation to last for 15
+    # seconds.
+    #
+    #session_timeout: "15s"
+
+
+{% if matrix_synapse_email_enabled %}
+# Configuration for sending emails from Synapse.
+#
+email:
+  # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'.
+  #
+  #smtp_host: mail.server
+  smtp_host: {{ matrix_synapse_email_smtp_host|string|to_json }}
+
+  # The port on the mail server for outgoing SMTP. Defaults to 25.
+  #
+  #smtp_port: 587
+  smtp_port: {{ matrix_synapse_email_smtp_port|to_json }}
+
+  # Username/password for authentication to the SMTP server. By default, no
+  # authentication is attempted.
+  #
+  #smtp_user: "exampleusername"
+  #smtp_pass: "examplepassword"
+
+  # Uncomment the following to require TLS transport security for SMTP.
+  # By default, Synapse will connect over plain text, and will then switch to
+  # TLS via STARTTLS *if the SMTP server supports it*. If this option is set,
+  # Synapse will refuse to connect unless the server supports STARTTLS.
+  #
+  #require_transport_security: true
+  require_transport_security: {{ matrix_synapse_email_smtp_require_transport_security|to_json }}
+
+  # Enable sending emails for messages that the user has missed
+  #
+  #enable_notifs: false
+  enable_notifs: true
+
+  # notif_from defines the "From" address to use when sending emails.
+  # It must be set if email sending is enabled.
+  #
+  # The placeholder '%(app)s' will be replaced by the application name,
+  # which is normally 'app_name' (below), but may be overridden by the
+  # Matrix client application.
+  #
+  # Note that the placeholder must be written '%(app)s', including the
+  # trailing 's'.
+  #
+  #notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
+  notif_from: {{ matrix_synapse_email_notif_from|string|to_json }}
+
+  # app_name defines the default value for '%(app)s' in notif_from and email
+  # subjects. It defaults to 'Matrix'.
+  #
+  #app_name: my_branded_matrix_server
+  app_name: Matrix
+
+  # Uncomment the following to disable automatic subscription to email
+  # notifications for new users. Enabled by default.
+  #
+  #notif_for_new_users: false
+  notif_for_new_users: True
+
+  # Custom URL for client links within the email notifications. By default
+  # links will be based on "https://matrix.to".
+  #
+  # (This setting used to be called riot_base_url; the old name is still
+  # supported for backwards-compatibility but is now deprecated.)
+  #
+  #client_base_url: "http://localhost/riot"
+  client_base_url: {{ matrix_synapse_email_client_base_url|string|to_json }}
+
+  # Configure the time that a validation email will expire after sending.
+  # Defaults to 1h.
+  #
+  #validation_token_lifetime: 15m
+
+  # Directory in which Synapse will try to find the template files below.
+  # If not set, or the files named below are not found within the template
+  # directory, default templates from within the Synapse package will be used.
+  #
+  # Synapse will look for the following templates in this directory:
+  #
+  # * The contents of email notifications of missed events: 'notif_mail.html' and
+  #   'notif_mail.txt'.
+  #
+  # * The contents of account expiry notice emails: 'notice_expiry.html' and
+  #   'notice_expiry.txt'.
+  #
+  # * The contents of password reset emails sent by the homeserver:
+  #   'password_reset.html' and 'password_reset.txt'
+  #
+  # * An HTML page that a user will see when they follow the link in the password
+  #   reset email. The user will be asked to confirm the action before their
+  #   password is reset: 'password_reset_confirmation.html'
+  #
+  # * HTML pages for success and failure that a user will see when they confirm
+  #   the password reset flow using the page above: 'password_reset_success.html'
+  #   and 'password_reset_failure.html'
+  #
+  # * The contents of address verification emails sent during registration:
+  #   'registration.html' and 'registration.txt'
+  #
+  # * HTML pages for success and failure that a user will see when they follow
+  #   the link in an address verification email sent during registration:
+  #   'registration_success.html' and 'registration_failure.html'
+  #
+  # * The contents of address verification emails sent when an address is added
+  #   to a Matrix account: 'add_threepid.html' and 'add_threepid.txt'
+  #
+  # * HTML pages for success and failure that a user will see when they follow
+  #   the link in an address verification email sent when an address is added
+  #   to a Matrix account: 'add_threepid_success.html' and
+  #   'add_threepid_failure.html'
+  #
+  # You can see the default templates at:
+  # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
+  #
+  #template_dir: "res/templates"
+
+  # Subjects to use when sending emails from Synapse.
+  #
+  # The placeholder '%(app)s' will be replaced with the value of the 'app_name'
+  # setting above, or by a value dictated by the Matrix client application.
+  #
+  # If a subject isn't overridden in this configuration file, the value used as
+  # its example will be used.
+  #
+  #subjects:
+
+    # Subjects for notification emails.
+    #
+    # On top of the '%(app)s' placeholder, these can use the following
+    # placeholders:
+    #
+    #   * '%(person)s', which will be replaced by the display name of the user(s)
+    #      that sent the message(s), e.g. "Alice and Bob".
+    #   * '%(room)s', which will be replaced by the name of the room the
+    #      message(s) have been sent to, e.g. "My super room".
+    #
+    # See the example provided for each setting to see which placeholder can be
+    # used and how to use them.
+    #
+    # Subject to use to notify about one message from one or more user(s) in a
+    # room which has a name.
+    #message_from_person_in_room: "[%(app)s] You have a message on %(app)s from %(person)s in the %(room)s room..."
+    #
+    # Subject to use to notify about one message from one or more user(s) in a
+    # room which doesn't have a name.
+    #message_from_person: "[%(app)s] You have a message on %(app)s from %(person)s..."
+    #
+    # Subject to use to notify about multiple messages from one or more users in
+    # a room which doesn't have a name.
+    #messages_from_person: "[%(app)s] You have messages on %(app)s from %(person)s..."
+    #
+    # Subject to use to notify about multiple messages in a room which has a
+    # name.
+    #messages_in_room: "[%(app)s] You have messages on %(app)s in the %(room)s room..."
+    #
+    # Subject to use to notify about multiple messages in multiple rooms.
+    #messages_in_room_and_others: "[%(app)s] You have messages on %(app)s in the %(room)s room and others..."
+    #
+    # Subject to use to notify about multiple messages from multiple persons in
+    # multiple rooms. This is similar to the setting above except it's used when
+    # the room in which the notification was triggered has no name.
+    #messages_from_person_and_others: "[%(app)s] You have messages on %(app)s from %(person)s and others..."
+    #
+    # Subject to use to notify about an invite to a room which has a name.
+    #invite_from_person_to_room: "[%(app)s] %(person)s has invited you to join the %(room)s room on %(app)s..."
+    #
+    # Subject to use to notify about an invite to a room which doesn't have a
+    # name.
+    #invite_from_person: "[%(app)s] %(person)s has invited you to chat on %(app)s..."
+
+    # Subject for emails related to account administration.
+    #
+    # On top of the '%(app)s' placeholder, these one can use the
+    # '%(server_name)s' placeholder, which will be replaced by the value of the
+    # 'server_name' setting in your Synapse configuration.
+    #
+    # Subject to use when sending a password reset email.
+    #password_reset: "[%(server_name)s] Password reset"
+    #
+    # Subject to use when sending a verification email to assert an address's
+    # ownership.
+    #email_validation: "[%(server_name)s] Validate your email"
+{% endif %}
+
+# Password providers allow homeserver administrators to integrate
+# their Synapse installation with existing authentication methods
+# ex. LDAP, external tokens, etc.
+#
+# For more information and known implementations, please see
+# https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md
+#
+# Note: instances wishing to use SAML or CAS authentication should
+# instead use the `saml2_config` or `cas_config` options,
+# respectively.
+#
+# password_providers:
+#    # Example config for an LDAP auth provider
+#    - module: "ldap_auth_provider.LdapAuthProvider"
+#      config:
+#        enabled: true
+#        uri: "ldap://ldap.example.com:389"
+#        start_tls: true
+#        base: "ou=users,dc=example,dc=com"
+#        attributes:
+#           uid: "cn"
+#           mail: "email"
+#           name: "givenName"
+#        #bind_dn:
+#        #bind_password:
+#        #filter: "(objectClass=posixAccount)"
+{% if matrix_synapse_password_providers_enabled %}
+password_providers:
+{% if matrix_synapse_ext_password_provider_shared_secret_auth_enabled %}
+  - module: "shared_secret_authenticator.SharedSecretAuthenticator"
+    config:
+      sharedSecret: {{ matrix_synapse_ext_password_provider_shared_secret_auth_shared_secret|string|to_json }}
+{% endif %}
+{% if matrix_synapse_ext_password_provider_rest_auth_enabled %}
+  - module: "rest_auth_provider.RestAuthProvider"
+    config:
+      endpoint: {{ matrix_synapse_ext_password_provider_rest_auth_endpoint|string|to_json }}
+      policy:
+        registration:
+          username:
+            enforceLowercase: {{ matrix_synapse_ext_password_provider_rest_auth_registration_enforce_lowercase }}
+          profile:
+            name: {{ matrix_synapse_ext_password_provider_rest_auth_registration_profile_name_autofill }}
+        login:
+          profile:
+            name: {{ matrix_synapse_ext_password_provider_rest_auth_login_profile_name_autofill }}
+{% endif %}
+{% if matrix_synapse_ext_password_provider_ldap_enabled %}
+  - module: "ldap_auth_provider.LdapAuthProvider"
+    config:
+      enabled: true
+      uri: {{ matrix_synapse_ext_password_provider_ldap_uri|string|to_json }}
+      start_tls: {{ matrix_synapse_ext_password_provider_ldap_start_tls|to_json }}
+      base: {{ matrix_synapse_ext_password_provider_ldap_base|string|to_json }}
+      active_directory: {{ matrix_synapse_ext_password_provider_ldap_active_directory|to_json }}
+      default_domain: {{ matrix_synapse_ext_password_provider_ldap_default_domain|string|to_json }}
+      attributes:
+        uid: {{ matrix_synapse_ext_password_provider_ldap_attributes_uid|string|to_json }}
+        mail: {{ matrix_synapse_ext_password_provider_ldap_attributes_mail|string|to_json }}
+        name: {{ matrix_synapse_ext_password_provider_ldap_attributes_name|string|to_json }}
+      bind_dn: {{ matrix_synapse_ext_password_provider_ldap_bind_dn|string|to_json }}
+      bind_password: {{ matrix_synapse_ext_password_provider_ldap_bind_password|string|to_json }}
+      filter: {{ matrix_synapse_ext_password_provider_ldap_filter|string|to_json }}
+{% endif %}
+{% endif %}
+
+
+## Push ##
+
+push:
+  # Clients requesting push notifications can either have the body of
+  # the message sent in the notification poke along with other details
+  # like the sender, or just the event ID and room ID (`event_id_only`).
+  # If clients choose the former, this option controls whether the
+  # notification request includes the content of the event (other details
+  # like the sender are still included). For `event_id_only` push, it
+  # has no effect.
+  #
+  # For modern android devices the notification content will still appear
+  # because it is loaded by the app. iPhone, however will send a
+  # notification saying only that a message arrived and who it came from.
+  #
+  # The default value is "true" to include message details. Uncomment to only
+  # include the event ID and room ID in push notification payloads.
+  #
+  include_content: {{ matrix_synapse_push_include_content|to_json }}
+
+  # When a push notification is received, an unread count is also sent.
+  # This number can either be calculated as the number of unread messages
+  # for the user, or the number of *rooms* the user has unread messages in.
+  #
+  # The default value is "true", meaning push clients will see the number of
+  # rooms with unread messages in them. Uncomment to instead send the number
+  # of unread messages.
+  #
+  #group_unread_count_by_room: false
+
+
+# Spam checkers are third-party modules that can block specific actions
+# of local users, such as creating rooms and registering undesirable
+# usernames, as well as remote users by redacting incoming events.
+#
+# spam_checker:
+   #- module: "my_custom_project.SuperSpamChecker"
+   #  config:
+   #    example_option: 'things'
+   #- module: "some_other_project.BadEventStopper"
+   #  config:
+   #    example_stop_events_from: ['@bad:example.com']
+spam_checker: {{ matrix_synapse_spam_checker|to_json }}
+
+
+## Rooms ##
+
+# Controls whether locally-created rooms should be end-to-end encrypted by
+# default.
+#
+# Possible options are "all", "invite", and "off". They are defined as:
+#
+# * "all": any locally-created room
+# * "invite": any room created with the "private_chat" or "trusted_private_chat"
+#             room creation presets
+# * "off": this option will take no effect
+#
+# The default value is "off".
+#
+# Note that this option will only affect rooms created after it is set. It
+# will also not affect rooms created by other servers.
+#
+#encryption_enabled_by_default_for_room_type: invite
+
+
+# Uncomment to allow non-server-admin users to create groups on this server
+#
+enable_group_creation: {{ matrix_synapse_enable_group_creation|to_json }}
+
+# If enabled, non server admins can only create groups with local parts
+# starting with this prefix
+#
+#group_creation_prefix: "unofficial_"
+
+
+
+# User Directory configuration
+#
+user_directory:
+    # Defines whether users can search the user directory. If false then
+    # empty responses are returned to all queries. Defaults to true.
+    #
+    # Uncomment to disable the user directory.
+    #
+    #enabled: false
+
+    # Defines whether to search all users visible to your HS when searching
+    # the user directory, rather than limiting to users visible in public
+    # rooms. Defaults to false.
+    #
+    # If you set it true, you'll have to rebuild the user_directory search
+    # indexes, see:
+    # https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md
+    #
+    # Uncomment to return search results containing all known users, even if that
+    # user does not share a room with the requester.
+    #
+    #search_all_users: true
+
+    # Defines whether to prefer local users in search query results.
+    # If True, local users are more likely to appear above remote users
+    # when searching the user directory. Defaults to false.
+    #
+    # Uncomment to prefer local over remote users in user directory search
+    # results.
+    #
+    #prefer_local_users: true
+
+
+# User Consent configuration
+#
+# for detailed instructions, see
+# https://github.com/matrix-org/synapse/blob/master/docs/consent_tracking.md
+#
+# Parts of this section are required if enabling the 'consent' resource under
+# 'listeners', in particular 'template_dir' and 'version'.
+#
+# 'template_dir' gives the location of the templates for the HTML forms.
+# This directory should contain one subdirectory per language (eg, 'en', 'fr'),
+# and each language directory should contain the policy document (named as
+# '<version>.html') and a success page (success.html).
+#
+# 'version' specifies the 'current' version of the policy document. It defines
+# the version to be served by the consent resource if there is no 'v'
+# parameter.
+#
+# 'server_notice_content', if enabled, will send a user a "Server Notice"
+# asking them to consent to the privacy policy. The 'server_notices' section
+# must also be configured for this to work. Notices will *not* be sent to
+# guest users unless 'send_server_notice_to_guests' is set to true.
+#
+# 'block_events_error', if set, will block any attempts to send events
+# until the user consents to the privacy policy. The value of the setting is
+# used as the text of the error.
+#
+# 'require_at_registration', if enabled, will add a step to the registration
+# process, similar to how captcha works. Users will be required to accept the
+# policy before their account is created.
+#
+# 'policy_name' is the display name of the policy users will see when registering
+# for an account. Has no effect unless `require_at_registration` is enabled.
+# Defaults to "Privacy Policy".
+#
+#user_consent:
+#  template_dir: res/templates/privacy
+#  version: 1.0
+#  server_notice_content:
+#    msgtype: m.text
+#    body: >-
+#      To continue using this homeserver you must review and agree to the
+#      terms and conditions at %(consent_uri)s
+#  send_server_notice_to_guests: True
+#  block_events_error: >-
+#    To continue using this homeserver you must review and agree to the
+#    terms and conditions at %(consent_uri)s
+#  require_at_registration: False
+#  policy_name: Privacy Policy
+#
+
+
+
+# Settings for local room and user statistics collection. See
+# docs/room_and_user_statistics.md.
+#
+stats:
+  # Uncomment the following to disable room and user statistics. Note that doing
+  # so may cause certain features (such as the room directory) not to work
+  # correctly.
+  #
+  #enabled: false
+
+  # The size of each timeslice in the room_stats_historical and
+  # user_stats_historical tables, as a time period. Defaults to "1d".
+  #
+  #bucket_size: 1h
+
+
+# Server Notices room configuration
+#
+# Uncomment this section to enable a room which can be used to send notices
+# from the server to users. It is a special room which cannot be left; notices
+# come from a special "notices" user id.
+#
+# If you uncomment this section, you *must* define the system_mxid_localpart
+# setting, which defines the id of the user which will be used to send the
+# notices.
+#
+# It's also possible to override the room name, the display name of the
+# "notices" user, and the avatar for the user.
+#
+#server_notices:
+#  system_mxid_localpart: notices
+#  system_mxid_display_name: "Server Notices"
+#  system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
+#  room_name: "Server Notices"
+
+
+
+# Uncomment to disable searching the public room list. When disabled
+# blocks searching local and remote room lists for local and remote
+# users by always returning an empty list for all queries.
+#
+#enable_room_list_search: false
+
+enable_room_list_search: {{ matrix_synapse_enable_room_list_search|to_json }}
+
+# The `alias_creation` option controls who's allowed to create aliases
+# on this server.
+#
+# The format of this option is a list of rules that contain globs that
+# match against user_id, room_id and the new alias (fully qualified with
+# server name). The action in the first rule that matches is taken,
+# which can currently either be "allow" or "deny".
+#
+# Missing user_id/room_id/alias fields default to "*".
+#
+# If no rules match the request is denied. An empty list means no one
+# can create aliases.
+#
+# Options for the rules include:
+#
+#   user_id: Matches against the creator of the alias
+#   alias: Matches against the alias being created
+#   room_id: Matches against the room ID the alias is being pointed at
+#   action: Whether to "allow" or "deny" the request if the rule matches
+#
+# The default is:
+#
+#alias_creation_rules:
+#  - user_id: "*"
+#    alias: "*"
+#    room_id: "*"
+#    action: allow
+
+alias_creation_rules: {{ matrix_synapse_alias_creation_rules|to_json }}
+
+# The `room_list_publication_rules` option controls who can publish and
+# which rooms can be published in the public room list.
+#
+# The format of this option is the same as that for
+# `alias_creation_rules`.
+#
+# If the room has one or more aliases associated with it, only one of
+# the aliases needs to match the alias rule. If there are no aliases
+# then only rules with `alias: *` match.
+#
+# If no rules match the request is denied. An empty list means no one
+# can publish rooms.
+#
+# Options for the rules include:
+#
+#   user_id: Matches against the creator of the alias
+#   room_id: Matches against the room ID being published
+#   alias: Matches against any current local or canonical aliases
+#            associated with the room
+#   action: Whether to "allow" or "deny" the request if the rule matches
+#
+# The default is:
+#
+#room_list_publication_rules:
+#  - user_id: "*"
+#    alias: "*"
+#    room_id: "*"
+#    action: allow
+
+room_list_publication_rules: {{ matrix_synapse_room_list_publication_rules|to_json }}
+
+
+# Server admins can define a Python module that implements extra rules for
+# allowing or denying incoming events. In order to work, this module needs to
+# override the methods defined in synapse/events/third_party_rules.py.
+#
+# This feature is designed to be used in closed federations only, where each
+# participating server enforces the same rules.
+#
+#third_party_event_rules:
+#  module: "my_custom_project.SuperRulesSet"
+#  config:
+#    example_option: 'things'
+
+
+## Opentracing ##
+
+# These settings enable opentracing, which implements distributed tracing.
+# This allows you to observe the causal chains of events across servers
+# including requests, key lookups etc., across any server running
+# synapse or any other other services which supports opentracing
+# (specifically those implemented with Jaeger).
+#
+opentracing:
+    # tracing is disabled by default. Uncomment the following line to enable it.
+    #
+    #enabled: true
+
+    # The list of homeservers we wish to send and receive span contexts and span baggage.
+    # See docs/opentracing.rst.
+    #
+    # This is a list of regexes which are matched against the server_name of the
+    # homeserver.
+    #
+    # By default, it is empty, so no servers are matched.
+    #
+    #homeserver_whitelist:
+    #  - ".*"
+
+    # A list of the matrix IDs of users whose requests will always be traced,
+    # even if the tracing system would otherwise drop the traces due to
+    # probabilistic sampling.
+    #
+    # By default, the list is empty.
+    #
+    #force_tracing_for_users:
+    #  - "@user1:server_name"
+    #  - "@user2:server_name"
+
+    # Jaeger can be configured to sample traces at different rates.
+    # All configuration options provided by Jaeger can be set here.
+    # Jaeger's configuration is mostly related to trace sampling which
+    # is documented here:
+    # https://www.jaegertracing.io/docs/latest/sampling/.
+    #
+    #jaeger_config:
+    #  sampler:
+    #    type: const
+    #    param: 1
+    #  logging:
+    #    false
+
+
+## Workers ##
+
+# Disables sending of outbound federation transactions on the main process.
+# Uncomment if using a federation sender worker.
+#
+#send_federation: false
+
+# It is possible to run multiple federation sender workers, in which case the
+# work is balanced across them.
+#
+# This configuration must be shared between all federation sender workers, and if
+# changed all federation sender workers must be stopped at the same time and then
+# started, to ensure that all instances are running with the same config (otherwise
+# events may be dropped).
+#
+#federation_sender_instances:
+#  - federation_sender1
+
+# When using workers this should be a map from `worker_name` to the
+# HTTP replication listener of the worker, if configured.
+#
+#instance_map:
+#  worker1:
+#    host: localhost
+#    port: 8034
+
+# Experimental: When using workers you can define which workers should
+# handle event persistence and typing notifications. Any worker
+# specified here must also be in the `instance_map`.
+#
+#stream_writers:
+#  events: worker1
+#  typing: worker1
+
+# The worker that is used to run background tasks (e.g. cleaning up expired
+# data). If not provided this defaults to the main process.
+#
+#run_background_tasks_on: worker1
+
+# A shared secret used by the replication APIs to authenticate HTTP requests
+# from workers.
+#
+# By default this is unused and traffic is not authenticated.
+#
+#worker_replication_secret: ""
+
+
+# Configuration for Redis when using workers. This *must* be enabled when
+# using workers (unless using old style direct TCP configuration).
+#
+redis:
+  # Uncomment the below to enable Redis support.
+  #
+  enabled: {{ matrix_synapse_redis_enabled }}
+
+  # Optional host and port to use to connect to redis. Defaults to
+  # localhost and 6379
+  #
+  host: {{ matrix_synapse_redis_host }}
+  port: {{ matrix_synapse_redis_port }}
+
+  # Optional password if configured on the Redis instance
+  #
+  password: {{ matrix_synapse_redis_password }}
+
+
+# vim:ft=yaml
diff --git a/roles/matrix-synapse/templates/synapse/synapse.log.config.j2 b/roles/matrix-synapse/templates/synapse/synapse.log.config.j2
new file mode 100644
index 000000000..09f07a2ea
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/synapse.log.config.j2
@@ -0,0 +1,36 @@
+#jinja2: lstrip_blocks: "True"
+
+version: 1
+
+formatters:
+    precise:
+        format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
+
+filters:
+    context:
+        (): synapse.util.logcontext.LoggingContextFilter
+        request: ""
+
+handlers:
+    console:
+        class: logging.StreamHandler
+        formatter: precise
+        filters: [context]
+
+loggers:
+    synapse:
+        level: {{ matrix_synapse_log_level }}
+
+    synapse.storage.SQL:
+        # beware: increasing this to DEBUG will make synapse log sensitive
+        # information such as access tokens.
+        level: {{ matrix_synapse_storage_sql_log_level }}
+
+{% for logger in matrix_synapse_additional_loggers %}
+    {{ logger.name }}:
+        level: {{ logger.level }}
+{% endfor %}
+
+root:
+    level: {{ matrix_synapse_root_log_level }}
+    handlers: [console]
diff --git a/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse-worker.service.j2 b/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse-worker.service.j2
new file mode 100644
index 000000000..6c90c9a3e
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse-worker.service.j2
@@ -0,0 +1,64 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Synapse worker ({{ matrix_synapse_worker_container_name }})
+AssertPathExists={{ matrix_synapse_config_dir_path }}/{{ matrix_synapse_worker_config_file_name }}
+After=matrix-synapse.service
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+
+ExecStartPre=-{{ matrix_host_command_docker }} kill {{ matrix_synapse_worker_container_name }}
+ExecStartPre=-{{ matrix_host_command_docker }} rm {{ matrix_synapse_worker_container_name }}
+
+# Intentional delay, so that the homeserver can manage to start.
+ExecStartPre={{ matrix_host_command_sleep }} 5
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name {{ matrix_synapse_worker_container_name }} \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			-e UID={{ matrix_user_uid }} \
+			-e GID={{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,noexec,nosuid,size={{ matrix_synapse_tmp_directory_size_mb }}m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_synapse_worker_details.port != 0 %}
+			--health-cmd 'curl -fSs http://localhost:{{ matrix_synapse_worker_details.port }}/health || exit 1' \
+			{% else %}
+			--no-healthcheck \
+			{% endif %}
+			{% if matrix_synapse_workers_enabled and matrix_synapse_workers_container_host_bind_address %}
+			{% if matrix_synapse_worker_details.port != 0 %}
+			-p {{ '' if matrix_synapse_workers_container_host_bind_address == '*' else (matrix_synapse_workers_container_host_bind_address + ':') }}{{ matrix_synapse_worker_details.port }}:{{ matrix_synapse_worker_details.port }} \
+			{% endif %}
+			{% if matrix_synapse_worker_details.metrics_port != 0 %}
+			-p {{ '' if matrix_synapse_workers_container_host_bind_address == '*' else (matrix_synapse_workers_container_host_bind_address + ':') }}{{ matrix_synapse_worker_details.metrics_port }}:{{ matrix_synapse_worker_details.metrics_port }} \
+			{% endif %}
+			{% endif %}
+			--mount type=bind,src={{ matrix_synapse_config_dir_path }},dst=/data,ro \
+			--mount type=bind,src={{ matrix_synapse_storage_path }},dst=/matrix-media-store-parent,bind-propagation=slave \
+			{% for volume in matrix_synapse_container_additional_volumes %}
+			-v {{ volume.src }}:{{ volume.dst }}:{{ volume.options }} \
+			{% endfor %}
+			{% for arg in matrix_synapse_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_synapse_docker_image }} \
+			run -m synapse.app.{{ matrix_synapse_worker_details.type }} -c /data/homeserver.yaml -c /data/{{ matrix_synapse_worker_config_file_name }}
+
+
+ExecStop=-{{ matrix_host_command_docker }} kill {{ matrix_synapse_worker_container_name }}
+ExecStop=-{{ matrix_host_command_docker }} rm {{ matrix_synapse_worker_container_name }}
+
+ExecReload={{ matrix_host_command_docker }} exec {{ matrix_synapse_worker_container_name }} /bin/sh -c 'kill -HUP 1'
+Restart=always
+RestartSec=30
+SyslogIdentifier={{ matrix_synapse_worker_container_name }}
+
+# Intentionally not making this WantedBy=matrix-synapse.service,
+# as matrix.synapse.service already has `Wants=` lines.
+# Also, WantedBy will trigger the creation of some `matrix-synapse.service.wants/` directory,
+# which we'd have to clean, etc. Better not.
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse.service.j2 b/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse.service.j2
new file mode 100644
index 000000000..2fbaac7b5
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/systemd/matrix-synapse.service.j2
@@ -0,0 +1,76 @@
+#jinja2: lstrip_blocks: "True"
+[Unit]
+Description=Synapse server
+{% for service in matrix_synapse_systemd_required_services_list %}
+Requires={{ service }}
+After={{ service }}
+
+{% endfor %}
+{% for service in matrix_synapse_systemd_wanted_services_list %}
+Wants={{ service }}
+{% endfor %}
+
+{% if matrix_synapse_workers_enabled %}
+{% for matrix_synapse_worker_details in matrix_synapse_workers_enabled_list %}
+Wants=matrix-synapse-worker-{{ matrix_synapse_worker_details.type }}-{{ matrix_synapse_worker_details.port }}.service
+{% endfor %}
+{% endif %}
+
+DefaultDependencies=no
+
+[Service]
+Type=simple
+Environment="HOME={{ matrix_systemd_unit_home_path }}"
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-synapse 2>/dev/null'
+ExecStartPre=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-synapse 2>/dev/null'
+{% if matrix_s3_media_store_enabled %}
+# Allow for some time before starting, so that media store can mount.
+# Mounting can happen later too, but if we start writing,
+# we'd write files to the local filesystem and fusermount will complain.
+ExecStartPre={{ matrix_host_command_sleep }} 3
+{% endif %}
+
+ExecStart={{ matrix_host_command_docker }} run --rm --name matrix-synapse \
+			--log-driver=none \
+			--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
+			--env=UID={{ matrix_user_uid }} \
+			--env=GID={{ matrix_user_gid }} \
+			--cap-drop=ALL \
+			--read-only \
+			--tmpfs=/tmp:rw,noexec,nosuid,size={{ matrix_synapse_tmp_directory_size_mb }}m \
+			--network={{ matrix_docker_network }} \
+			{% if matrix_synapse_container_client_api_host_bind_port %}
+			-p {{ matrix_synapse_container_client_api_host_bind_port }}:8008 \
+			{% endif %}
+			{% if matrix_synapse_federation_enabled and matrix_synapse_tls_federation_listener_enabled and matrix_synapse_container_federation_api_tls_host_bind_port %}
+			-p {{ matrix_synapse_container_federation_api_tls_host_bind_port }}:8448 \
+			{% endif %}
+			{% if matrix_synapse_federation_enabled and matrix_synapse_container_federation_api_plain_host_bind_port %}
+			-p {{ matrix_synapse_container_federation_api_plain_host_bind_port }}:8048 \
+			{% endif %}
+			{% if matrix_synapse_metrics_enabled and matrix_synapse_container_metrics_api_host_bind_port %}
+			-p {{ matrix_synapse_container_metrics_api_host_bind_port }}:{{ matrix_synapse_metrics_port }} \
+			{% endif %}
+			{% if matrix_synapse_manhole_enabled and matrix_synapse_container_manhole_api_host_bind_port %}
+			-p {{ matrix_synapse_container_manhole_api_host_bind_port }}:9000 \
+			{% endif %}
+			--mount type=bind,src={{ matrix_synapse_config_dir_path }},dst=/data,ro \
+			--mount type=bind,src={{ matrix_synapse_storage_path }},dst=/matrix-media-store-parent,bind-propagation=slave \
+			{% for volume in matrix_synapse_container_additional_volumes %}
+			-v {{ volume.src }}:{{ volume.dst }}:{{ volume.options }} \
+			{% endfor %}
+			{% for arg in matrix_synapse_container_extra_arguments %}
+			{{ arg }} \
+			{% endfor %}
+			{{ matrix_synapse_docker_image }} \
+			run -m synapse.app.homeserver -c /data/homeserver.yaml
+
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} kill matrix-synapse 2>/dev/null'
+ExecStop=-{{ matrix_host_command_sh }} -c '{{ matrix_host_command_docker }} rm matrix-synapse 2>/dev/null'
+ExecReload={{ matrix_host_command_docker }} exec matrix-synapse /bin/sh -c 'kill -HUP 1'
+Restart=always
+RestartSec=30
+SyslogIdentifier=matrix-synapse
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/matrix-synapse/templates/synapse/usr-local-bin/matrix-synapse-register-user.j2 b/roles/matrix-synapse/templates/synapse/usr-local-bin/matrix-synapse-register-user.j2
new file mode 100644
index 000000000..456c0667a
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/usr-local-bin/matrix-synapse-register-user.j2
@@ -0,0 +1,17 @@
+#jinja2: lstrip_blocks: "True"
+#!/bin/bash
+
+if [ $# -ne 3 ]; then
+	echo "Usage: "$0" <username> <password> <admin access: 0 or 1>"
+	exit 1
+fi
+
+user=$1
+password=$2
+admin=$3
+
+if [ "$admin" -eq "1" ]; then
+	docker exec matrix-synapse register_new_matrix_user -u "$user" -p "$password" -c /data/homeserver.yaml --admin http://localhost:8008
+else
+	docker exec matrix-synapse register_new_matrix_user -u "$user" -p "$password" -c /data/homeserver.yaml --no-admin http://localhost:8008
+fi
diff --git a/roles/matrix-synapse/templates/synapse/worker.yaml.j2 b/roles/matrix-synapse/templates/synapse/worker.yaml.j2
new file mode 100644
index 000000000..36ae5a7e6
--- /dev/null
+++ b/roles/matrix-synapse/templates/synapse/worker.yaml.j2
@@ -0,0 +1,45 @@
+#jinja2: lstrip_blocks: "True"
+worker_app: synapse.app.{{ matrix_synapse_worker_details.type }}
+worker_name: {{ matrix_synapse_worker_details.type ~ ':' ~ matrix_synapse_worker_details.port }}
+
+{% if matrix_synapse_replication_listener_enabled %}
+worker_replication_host: matrix-synapse
+worker_replication_http_port: {{ matrix_synapse_replication_http_port }}
+{% endif %}
+
+{% set has_listeners = (matrix_synapse_worker_details.type not in [ 'appservice', 'federation_sender', 'pusher' ] or matrix_synapse_metrics_enabled) %}
+
+{% set http_resources = [] %}
+
+{% if matrix_synapse_worker_details.type in ['generic_worker', 'frontend_proxy', 'user_dir'] %}
+  {% set http_resources = http_resources + ['client'] %}
+{% endif %}
+{% if matrix_synapse_worker_details.type in ['generic_worker'] %}
+  {% set http_resources = http_resources+ ['federation'] %}
+{% endif %}
+{% if matrix_synapse_worker_details.type in ['media_repository'] %}
+  {% set http_resources = http_resources + ['media'] %}
+{% endif %}
+
+{% if http_resources|length > 0 or matrix_synapse_metrics_enabled %}
+worker_listeners:
+{% if http_resources|length > 0 %}
+  - type: http
+    bind_addresses: ['::']
+    port: {{ matrix_synapse_worker_details.port }}
+    resources:
+      - names: {{ http_resources|to_json }}
+{% endif %}
+{% if matrix_synapse_metrics_enabled %}
+  - type: metrics
+    bind_addresses: ['0.0.0.0']
+    port: {{ matrix_synapse_worker_details.metrics_port }}
+{% endif %}
+{% endif %}
+
+{% if matrix_synapse_worker_details.type == 'frontend_proxy' %}
+worker_main_http_uri: http://matrix-synapse:8008
+{% endif %}
+
+worker_daemonize: false
+worker_log_config: /data/{{ matrix_server_fqn_matrix }}.log.config
diff --git a/roles/matrix-synapse/vars/main.yml b/roles/matrix-synapse/vars/main.yml
new file mode 100644
index 000000000..5839aa81b
--- /dev/null
+++ b/roles/matrix-synapse/vars/main.yml
@@ -0,0 +1,34 @@
+---
+
+matrix_synapse_client_api_url_endpoint_public: "https://{{ matrix_server_fqn_matrix }}/_matrix/client/versions"
+matrix_synapse_federation_api_url_endpoint_public: "https://{{ matrix_server_fqn_matrix }}:{{ matrix_federation_public_port }}/_matrix/federation/v1/version"
+
+# Tells whether this role had executed or not. Toggled to `true` during runtime.
+matrix_synapse_role_executed: false
+
+matrix_synapse_media_store_directory_name: "{{ matrix_synapse_media_store_path|basename }}"
+
+# A Synapse generic worker can handle both federation and client-server API endpoints.
+# We wish to split these, as we normally serve federation separately and don't want them mixed up.
+#
+# This is some ugly Ansible/Jinja2 hack (seen here: https://stackoverflow.com/a/47831492),
+# which takes a list of various strings and removes the ones NOT containing `/_matrix/client` anywhere in them.
+#
+# We intentionally don't do a diff between everything possible (`matrix_synapse_workers_generic_worker_endpoints`) and `matrix_synapse_workers_generic_worker_federation_endpoints`,
+# because `matrix_synapse_workers_generic_worker_endpoints` also contains things like `/_synapse/client/`, etc.
+# While /_synapse/client/ endpoints are somewhat client-server API-related, they're:
+# - neither part of the client-server API spec (and are thus, different)
+# - nor always OK to forward to a worker (we're supposed to obey `matrix_nginx_proxy_proxy_matrix_client_api_forwarded_location_synapse_client_api_enabled`)
+#
+# It's also not too many of these APIs (only `^/_synapse/client/password_reset/email/submit_token$` at the time of this writing / 2021-01-24),
+# so it's not that important whether we forward them or not.
+#
+# Basically, we aim to cover most things. Skipping `/_synapse/client` or a few other minor things doesn't matter too much.
+matrix_synapse_workers_generic_worker_client_server_endpoints: "{{ matrix_synapse_workers_generic_worker_endpoints|default([]) | map('regex_search', '.*/_matrix/client.*')| list | difference([none]) }}"
+
+# A Synapse generic worker can handle both federation and client-server API endpoints.
+# We wish to split these, as we normally serve federation separately and don't want them mixed up.
+#
+# This is some ugly Ansible/Jinja2 hack (seen here: https://stackoverflow.com/a/47831492),
+# which takes a list of various strings and removes the ones NOT containing `/_matrix/federation` or `/_matrix/key` anywhere in them.
+matrix_synapse_workers_generic_worker_federation_endpoints: "{{ matrix_synapse_workers_generic_worker_endpoints|default([]) | map('regex_search', '.*(/_matrix/federation|/_matrix/key).*')| list | difference([none]) }}"
diff --git a/roles/matrix-synapse/vars/workers.yml b/roles/matrix-synapse/vars/workers.yml
new file mode 100644
index 000000000..1f817c8eb
--- /dev/null
+++ b/roles/matrix-synapse/vars/workers.yml
@@ -0,0 +1,322 @@
+---
+
+matrix_synapse_workers_generic_worker_endpoints:
+  # This worker can handle API requests matching the following regular
+  # expressions:
+
+  # Sync requests
+  - ^/_matrix/client/(v2_alpha|r0)/sync$
+  - ^/_matrix/client/(api/v1|v2_alpha|r0)/events$
+  - ^/_matrix/client/(api/v1|r0)/initialSync$
+  - ^/_matrix/client/(api/v1|r0)/rooms/[^/]+/initialSync$
+
+  # Federation requests
+  - ^/_matrix/federation/v1/event/
+  - ^/_matrix/federation/v1/state/
+  - ^/_matrix/federation/v1/state_ids/
+  - ^/_matrix/federation/v1/backfill/
+  - ^/_matrix/federation/v1/get_missing_events/
+  - ^/_matrix/federation/v1/publicRooms
+  - ^/_matrix/federation/v1/query/
+  - ^/_matrix/federation/v1/make_join/
+  - ^/_matrix/federation/v1/make_leave/
+  - ^/_matrix/federation/v1/send_join/
+  - ^/_matrix/federation/v2/send_join/
+  - ^/_matrix/federation/v1/send_leave/
+  - ^/_matrix/federation/v2/send_leave/
+  - ^/_matrix/federation/v1/invite/
+  - ^/_matrix/federation/v2/invite/
+  - ^/_matrix/federation/v1/query_auth/
+  - ^/_matrix/federation/v1/event_auth/
+  - ^/_matrix/federation/v1/exchange_third_party_invite/
+  - ^/_matrix/federation/v1/user/devices/
+  - ^/_matrix/federation/v1/get_groups_publicised$
+  - ^/_matrix/key/v2/query
+
+  # Inbound federation transaction request
+  - ^/_matrix/federation/v1/send/
+
+  # Client API requests
+  - ^/_matrix/client/(api/v1|r0|unstable)/publicRooms$
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/joined_members$
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/context/.*$
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/members$
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/state$
+  - ^/_matrix/client/(api/v1|r0|unstable)/account/3pid$
+  - ^/_matrix/client/(api/v1|r0|unstable)/devices$
+  - ^/_matrix/client/(api/v1|r0|unstable)/keys/query$
+  - ^/_matrix/client/(api/v1|r0|unstable)/keys/changes$
+  - ^/_matrix/client/versions$
+  - ^/_matrix/client/(api/v1|r0|unstable)/voip/turnServer$
+  - ^/_matrix/client/(api/v1|r0|unstable)/joined_groups$
+  - ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$
+  - ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/event/
+  - ^/_matrix/client/(api/v1|r0|unstable)/joined_rooms$
+  - ^/_matrix/client/(api/v1|r0|unstable)/search$
+
+  # Registration/login requests
+  - ^/_matrix/client/(api/v1|r0|unstable)/login$
+  - ^/_matrix/client/(r0|unstable)/register$
+
+  # Event sending requests
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/redact
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/send
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/state/
+  - ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$
+  - ^/_matrix/client/(api/v1|r0|unstable)/join/
+  - ^/_matrix/client/(api/v1|r0|unstable)/profile/
+
+
+  # Additionally, the following REST endpoints can be handled for GET requests:
+
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_matrix/federation/v1/groups/
+
+  # Pagination requests can also be handled, but all requests for a given
+  # room must be routed to the same instance. Additionally, care must be taken to
+  # ensure that the purge history admin API is not used while pagination requests
+  # for the room are in flight:
+
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/messages$
+
+  # Additionally, the following endpoints should be included if Synapse is configured
+  # to use SSO (you only need to include the ones for whichever SSO provider you're
+  # using):
+
+  # for all SSO providers
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_matrix/client/(api/v1|r0|unstable)/login/sso/redirect
+  # ^/_synapse/client/pick_idp$
+  # ^/_synapse/client/pick_username
+  # ^/_synapse/client/new_user_consent$
+  # ^/_synapse/client/sso_register$
+
+  # OpenID Connect requests.
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_synapse/client/oidc/callback$
+
+  # SAML requests.
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_synapse/client/saml2/authn_response$
+
+  # CAS requests.
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_matrix/client/(api/v1|r0|unstable)/login/cas/ticket$
+
+  # Ensure that all SSO logins go to a single process.
+  # For multiple workers not handling the SSO endpoints properly, see
+  # [#7530](https://github.com/matrix-org/synapse/issues/7530) and
+  # [#9427](https://github.com/matrix-org/synapse/issues/9427).
+
+  # Note that a HTTP listener with `client` and `federation` resources must be
+  # configured in the `worker_listeners` option in the worker config.
+
+  # #### Load balancing
+
+  # It is possible to run multiple instances of this worker app, with incoming requests
+  # being load-balanced between them by the reverse-proxy. However, different endpoints
+  # have different characteristics and so admins
+  # may wish to run multiple groups of workers handling different endpoints so that
+  # load balancing can be done in different ways.
+
+  # For `/sync` and `/initialSync` requests it will be more efficient if all
+  # requests from a particular user are routed to a single instance. Extracting a
+  # user ID from the access token or `Authorization` header is currently left as an
+  # exercise for the reader. Admins may additionally wish to separate out `/sync`
+  # requests that have a `since` query parameter from those that don't (and
+  # `/initialSync`), as requests that don't are known as "initial sync" that happens
+  # when a user logs in on a new device and can be *very* resource intensive, so
+  # isolating these requests will stop them from interfering with other users ongoing
+  # syncs.
+
+  # Federation and client requests can be balanced via simple round robin.
+
+  # The inbound federation transaction request `^/_matrix/federation/v1/send/`
+  # should be balanced by source IP so that transactions from the same remote server
+  # go to the same process.
+
+  # Registration/login requests can be handled separately purely to help ensure that
+  # unexpected load doesn't affect new logins and sign ups.
+
+  # Finally, event sending requests can be balanced by the room ID in the URI (or
+  # the full URI, or even just round robin), the room ID is the path component after
+  # `/rooms/`. If there is a large bridge connected that is sending or may send lots
+  # of events, then a dedicated set of workers can be provisioned to limit the
+  # effects of bursts of events from that bridge on events sent by normal users.
+
+  # #### Stream writers
+
+  # Additionally, there is *experimental* support for moving writing of specific
+  # streams (such as events) off of the main process to a particular worker. (This
+  # is only supported with Redis-based replication.)
+
+  # Currently supported streams are `events` and `typing`.
+
+  # To enable this, the worker must have a HTTP replication listener configured,
+  # have a `worker_name` and be listed in the `instance_map` config. For example to
+  # move event persistence off to a dedicated worker, the shared configuration would
+  # include:
+
+  # ```yaml
+  # instance_map:
+  #     event_persister1:
+  #         host: localhost
+  #         port: 8034
+
+  # stream_writers:
+  #     events: event_persister1
+  # ```
+
+  # The `events` stream also experimentally supports having multiple writers, where
+  # work is sharded between them by room ID. Note that you *must* restart all worker
+  # instances when adding or removing event persisters. An example `stream_writers`
+  # configuration with multiple writers:
+
+  # ```yaml
+  # stream_writers:
+  #     events:
+  #         - event_persister1
+  #         - event_persister2
+  # ```
+
+  # #### Background tasks
+
+  # There is also *experimental* support for moving background tasks to a separate
+  # worker. Background tasks are run periodically or started via replication. Exactly
+  # which tasks are configured to run depends on your Synapse configuration (e.g. if
+  # stats is enabled).
+
+  # To enable this, the worker must have a `worker_name` and can be configured to run
+  # background tasks. For example, to move background tasks to a dedicated worker,
+  # the shared configuration would include:
+
+  # ```yaml
+  # run_background_tasks_on: background_worker
+  # ```
+
+  # You might also wish to investigate the `update_user_directory` and
+  # `media_instance_running_background_jobs` settings.
+
+# pusher worker (no API endpoints) [
+  # Handles sending push notifications to sygnal and email. Doesn't handle any
+  # REST endpoints itself, but you should set `start_pushers: False` in the
+  # shared configuration file to stop the main synapse sending push notifications.
+
+  # To run multiple instances at once the `pusher_instances` option should list all
+  # pusher instances by their worker name, e.g.:
+
+  # ```yaml
+  # pusher_instances:
+  #     - pusher_worker1
+  #     - pusher_worker2
+  # ```
+
+# ]
+
+# appservice worker (no API endpoints) [
+  # Handles sending output traffic to Application Services. Doesn't handle any
+  # REST endpoints itself, but you should set `notify_appservices: False` in the
+  # shared configuration file to stop the main synapse sending appservice notifications.
+
+  # Note this worker cannot be load-balanced: only one instance should be active.
+
+# ]
+
+# federation_sender worker (no API endpoints) [
+  # Handles sending federation traffic to other servers. Doesn't handle any
+  # REST endpoints itself, but you should set `send_federation: False` in the
+  # shared configuration file to stop the main synapse sending this traffic.
+
+  # If running multiple federation senders then you must list each
+  # instance in the `federation_sender_instances` option by their `worker_name`.
+  # All instances must be stopped and started when adding or removing instances.
+  # For example:
+
+  # ```yaml
+  # federation_sender_instances:
+  #     - federation_sender1
+  #     - federation_sender2
+  # ```
+# ]
+
+matrix_synapse_workers_media_repository_endpoints:
+  # Handles the media repository. It can handle all endpoints starting with:
+
+  - ^/_matrix/media/
+
+  # ... and the following regular expressions matching media-specific administration APIs:
+
+  - ^/_synapse/admin/v1/purge_media_cache$
+  - ^/_synapse/admin/v1/room/.*/media.*$
+  - ^/_synapse/admin/v1/user/.*/media.*$
+  - ^/_synapse/admin/v1/media/.*$
+  - ^/_synapse/admin/v1/quarantine_media/.*$
+
+  # You should also set `enable_media_repo: False` in the shared configuration
+  # file to stop the main synapse running background jobs related to managing the
+  # media repository.
+
+  # In the `media_repository` worker configuration file, configure the http listener to
+  # expose the `media` resource. For example:
+
+  # ```yaml
+  #     worker_listeners:
+  #      - type: http
+  #        port: 8085
+  #        resources:
+  #          - names:
+  #            - media
+  # ```
+
+  # Note that if running multiple media repositories they must be on the same server
+  # and you must configure a single instance to run the background tasks, e.g.:
+
+  # ```yaml
+  #     media_instance_running_background_jobs: "media-repository-1"
+  # ```
+
+  # Note that if a reverse proxy is used , then `/_matrix/media/` must be routed for both inbound client and federation requests (if they are handled separately).
+
+matrix_synapse_workers_user_dir_endpoints:
+  # Handles searches in the user directory. It can handle REST endpoints matching
+  # the following regular expressions:
+
+  - ^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$
+
+  # When using this worker you must also set `update_user_directory: False` in the
+  # shared configuration file to stop the main synapse running background
+  # jobs related to updating the user directory.
+
+matrix_synapse_workers_frontend_proxy_endpoints:
+  # Proxies some frequently-requested client endpoints to add caching and remove
+  # load from the main synapse. It can handle REST endpoints matching the following
+  # regular expressions:
+
+  - ^/_matrix/client/(api/v1|r0|unstable)/keys/upload
+
+  # If `use_presence` is False in the homeserver config, it can also handle REST
+  # endpoints matching the following regular expressions:
+
+  # FIXME: ADDITIONAL CONDITIONS REQUIRED: to be enabled manually
+  # ^/_matrix/client/(api/v1|r0|unstable)/presence/[^/]+/status
+
+  # This "stub" presence handler will pass through `GET` request but make the
+  # `PUT` effectively a no-op.
+
+  # It will proxy any requests it cannot handle to the main synapse instance. It
+  # must therefore be configured with the location of the main instance, via
+  # the `worker_main_http_uri` setting in the `frontend_proxy` worker configuration
+  # file. For example:
+
+  #     worker_main_http_uri: http://127.0.0.1:8008
+
+matrix_synapse_workers_avail_list:
+  - appservice
+  - federation_sender
+  - frontend_proxy
+  - generic_worker
+  - media_repository
+  - pusher
+  - user_dir
diff --git a/setup.yml b/setup.yml
new file mode 100755
index 000000000..142364c46
--- /dev/null
+++ b/setup.yml
@@ -0,0 +1,58 @@
+---
+- name: "Set up a Matrix server"
+  hosts: "{{ target if target is defined else 'matrix_servers' }}"
+  become: true
+
+  vars_files:
+    - roles/matrix-synapse/vars/workers.yml
+
+  roles:
+    - matrix-awx
+    - matrix-base
+    - matrix-dynamic-dns
+    - matrix-mailer
+    - matrix-postgres
+    - matrix-redis
+    - matrix-corporal
+    - matrix-bridge-appservice-discord
+    - matrix-bridge-appservice-slack
+    - matrix-bridge-appservice-webhooks
+    - matrix-bridge-appservice-irc
+    - matrix-bridge-mautrix-facebook
+    - matrix-bridge-mautrix-hangouts
+    - matrix-bridge-mautrix-instagram
+    - matrix-bridge-mautrix-signal
+    - matrix-bridge-mautrix-telegram
+    - matrix-bridge-mautrix-whatsapp
+    - matrix-bridge-mx-puppet-discord
+    - matrix-bridge-mx-puppet-groupme
+    - matrix-bridge-mx-puppet-steam
+    - matrix-bridge-mx-puppet-skype
+    - matrix-bridge-mx-puppet-slack
+    - matrix-bridge-mx-puppet-twitter
+    - matrix-bridge-mx-puppet-instagram
+    - matrix-bridge-sms
+    - matrix-bridge-heisenbridge
+    - matrix-bot-matrix-reminder-bot
+    - matrix-bot-go-neb
+    - matrix-bot-mjolnir
+    - matrix-synapse
+    - matrix-synapse-admin
+    - matrix-prometheus-node-exporter
+    - matrix-prometheus
+    - matrix-grafana
+    - matrix-registration
+    - matrix-client-element
+    - matrix-client-hydrogen
+    - matrix-jitsi
+    - matrix-ma1sd
+    - matrix-dimension
+    - matrix-etherpad
+    - matrix-email2matrix
+    - matrix-sygnal
+    - matrix-nginx-proxy
+    - matrix-coturn
+    - matrix-aux
+    - matrix-postgres-backup
+    - matrix-prometheus-postgres-exporter
+    - matrix-common-after
\ No newline at end of file