3
0
mirror of https://github.com/spantaleev/matrix-docker-ansible-deploy.git synced 2026-02-28 09:53:09 +00:00

Merge Synapse reverse-proxy companion role into matrix-synapse

The companion role was tightly coupled to Synapse through shared tags, worker routing, and lifecycle ordering. Keeping them separate added coordination overhead without practical benefits, especially for parallelized execution.

This merges the role into matrix-synapse while keeping companion logic organized under dedicated reverse_proxy_companion task/template subdirectories.

Compatibility is preserved:
- matrix_synapse_reverse_proxy_companion_* variable names remain unchanged
- install/setup companion-specific tags remain available

Cross-role/global wiring is now in group_vars (matrix-synapse section), while role defaults provide sensible standalone defaults and self-wiring for Synapse-owned values.
This commit is contained in:
Slavi Pantaleev
2026-02-26 06:51:47 +02:00
parent 63b6bf4bc1
commit 28afbde971
23 changed files with 436 additions and 460 deletions

View File

@@ -0,0 +1,224 @@
{#
SPDX-FileCopyrightText: 2024 Slavi Pantaleev
SPDX-FileCopyrightText: 2024 - 2025 Catalan Lover <catalanlover@protonmail.com>
SPDX-License-Identifier: AGPL-3.0-or-later
#}
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_enabled %}
traefik.enable=true
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_docker_network %}
traefik.docker.network={{ matrix_synapse_reverse_proxy_companion_container_labels_traefik_docker_network }}
{% endif %}
traefik.http.services.matrix-synapse-reverse-proxy-companion-client-api.loadbalancer.server.port=8008
traefik.http.services.matrix-synapse-reverse-proxy-companion-federation-api.loadbalancer.server.port=8048
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_enabled %}
############################################################
# #
# Public Client-API (/_matrix) #
# #
############################################################
{% set client_api_middlewares = [] %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_enabled %}
{% set client_api_middlewares = client_api_middlewares + [matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_name] %}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_rule }}
{% if client_api_middlewares | length > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.middlewares={{ client_api_middlewares | join(',') }}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.service=matrix-synapse-reverse-proxy-companion-client-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_entrypoints }}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.tls={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_tls | to_json }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_tls %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-api.tls.certResolver={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_api_traefik_tls_certResolver }}
{% endif %}
############################################################
# #
# /Public Client-API (/_matrix) #
# #
############################################################
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_internal_client_api_enabled %}
############################################################
# #
# Internal Client-API (/_matrix) #
# #
############################################################
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_api_traefik_rule }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_internal_client_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-api.service=matrix-synapse-reverse-proxy-companion-client-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_api_traefik_entrypoints }}
############################################################
# #
# /Internal Client-API (/_matrix) #
# #
############################################################
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_enabled %}
############################################################
# #
# Public Synapse Client API (/_synapse/client) #
# #
############################################################
{% set synapse_client_api_middlewares = [] %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_enabled %}
{% set synapse_client_api_middlewares = synapse_client_api_middlewares + [matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_name] %}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_rule }}
{% if synapse_client_api_middlewares | length > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.middlewares={{ synapse_client_api_middlewares | join(',') }}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.service=matrix-synapse-reverse-proxy-companion-client-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_entrypoints }}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.tls={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_tls | to_json }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_tls %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-client-api.tls.certResolver={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_client_api_traefik_tls_certResolver }}
{% endif %}
############################################################
# #
# /Public Synapse Client API (/_synapse/client) #
# #
############################################################
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_enabled %}
############################################################
# #
# Public Synapse Admin API (/_synapse/admin) #
# #
############################################################
{% set synapse_admin_api_middlewares = [] %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_enabled %}
{% set synapse_admin_api_middlewares = synapse_admin_api_middlewares + [matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_name] %}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_rule }}
{% if synapse_admin_api_middlewares | length > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.middlewares={{ synapse_admin_api_middlewares | join(',') }}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.service=matrix-synapse-reverse-proxy-companion-client-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_entrypoints }}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.tls={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_tls | to_json }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_tls %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.tls.certResolver={{ matrix_synapse_reverse_proxy_companion_container_labels_public_client_synapse_admin_api_traefik_tls_certResolver }}
{% endif %}
############################################################
# #
# /Public Synapse Admin API (/_synapse/admin) #
# #
############################################################
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_internal_client_synapse_admin_api_enabled %}
############################################################
# #
# Internal Synapse Admin API (/_synapse/admin) #
# #
############################################################
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-synapse-admin-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_synapse_admin_api_traefik_rule }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_internal_client_synapse_admin_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-client-synapse-admin-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_synapse_admin_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-synapse-admin-api.service=matrix-synapse-reverse-proxy-companion-client-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-internal-client-synapse-admin-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_internal_client_synapse_admin_api_traefik_entrypoints }}
############################################################
# #
# /Internal Synapse Admin API (/_synapse/admin) #
# #
############################################################
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_enabled %}
############################################################
# #
# Public Federation-API (/_matrix) #
# #
############################################################
{% set federation_api_middlewares = [] %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_enabled %}
{% set federation_api_middlewares = federation_api_middlewares + [matrix_synapse_reverse_proxy_companion_container_labels_traefik_compression_middleware_name] %}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.rule={{ matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_rule }}
{% if federation_api_middlewares | length > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.middlewares={{ federation_api_middlewares | join(',') }}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_priority | int > 0 %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.priority={{ matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.service=matrix-synapse-reverse-proxy-companion-federation-api
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.entrypoints={{ matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_entrypoints }}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.tls={{ matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_tls | to_json }}
{% if matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_tls %}
traefik.http.routers.matrix-synapse-reverse-proxy-companion-public-federation-api.tls.certResolver={{ matrix_synapse_reverse_proxy_companion_container_labels_public_federation_api_traefik_tls_certResolver }}
{% endif %}
############################################################
# #
# /Public Federation-API (/_matrix) #
# #
############################################################
{% endif %}
{% endif %}
{{ matrix_synapse_reverse_proxy_companion_container_labels_additional_labels }}

View File

@@ -0,0 +1,354 @@
#jinja2: lstrip_blocks: True
{% set room_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'room_worker') | list %}
{% set sync_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'sync_worker') | list %}
{% set client_reader_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'client_reader') | list %}
{% set federation_reader_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'federation_reader') | list %}
{% set generic_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'generic_worker') | list %}
{% set stream_writer_typing_stream_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'stream_writer') | selectattr('stream_writer_stream', 'equalto', 'typing') | list %}
{% set stream_writer_to_device_stream_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'stream_writer') | selectattr('stream_writer_stream', 'equalto', 'to_device') | list %}
{% set stream_writer_account_data_stream_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'stream_writer') | selectattr('stream_writer_stream', 'equalto', 'account_data') | list %}
{% set stream_writer_receipts_stream_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'stream_writer') | selectattr('stream_writer_stream', 'equalto', 'receipts') | list %}
{% set stream_writer_presence_stream_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'stream_writer') | selectattr('stream_writer_stream', 'equalto', 'presence') | list %}
{% set media_repository_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'media_repository') | list %}
{% set user_dir_workers = matrix_synapse_reverse_proxy_companion_synapse_workers_list | selectattr('type', 'equalto', 'user_dir') | list %}
{% macro render_worker_upstream(name, workers, load_balance) %}
upstream {{ name }} {
{#
We need to use a zone so that the upstream is stored in shared memory,
otherwise we can't use `resolve` below, as reported by nginx:
> resolving names at run time requires upstream ".." in ... to be in shared memory
#}
zone {{ name }} 64k;
{{ load_balance }}
keepalive {{ ((workers | length) * 2) | string }};
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
{% for worker in workers %}
server "{{ worker.name }}:{{ worker.port }}" resolve;
{% endfor %}
}
{% endmacro %}
{% macro render_locations_to_upstream(locations, upstream_name) %}
{% for location in locations %}
location ~ {{ location }} {
proxy_pass http://{{ upstream_name }}$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
{% endfor %}
{% endmacro %}
{% macro render_locations_to_upstream_with_whoami_sync_worker_router(locations, upstream_name) %}
{% for location in locations %}
location ~ {{ location }} {
# Use auth_request to call the whoami sync worker router.
# The handler resolves the access token to a user identifier and returns it
# in the X-User-Identifier header, which is then used for upstream hashing.
auth_request /_whoami_sync_worker_router;
auth_request_set $user_identifier $sent_http_x_user_identifier;
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_debug_headers_enabled %}
add_header X-Sync-Worker-Router-User-Identifier $user_identifier always;
add_header X-Sync-Worker-Router-Upstream $upstream_addr always;
{% endif %}
proxy_pass http://{{ upstream_name }}$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
{% endfor %}
{% endmacro %}
{% if matrix_synapse_reverse_proxy_companion_synapse_workers_enabled %}
# Whether to upgrade HTTP connection
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
#Extract room name from URI
map $request_uri $room_name {
~^/_matrix/(client|federation)/.*?(?:%21|!)(?<room>[A-Za-z0-9._=\-\/]+)(?::|%3A)[A-Za-z0-9._=\-\/]+ $room;
}
# End maps
{% if matrix_synapse_reverse_proxy_companion_synapse_cache_enabled %}
proxy_cache_path {{ matrix_synapse_reverse_proxy_companion_synapse_cache_path }} levels=1:2 keys_zone={{ matrix_synapse_reverse_proxy_companion_synapse_cache_keys_zone_name }}:{{ matrix_synapse_reverse_proxy_companion_synapse_cache_keys_zone_size }} inactive={{ matrix_synapse_reverse_proxy_companion_synapse_cache_inactive_time }} max_size={{ matrix_synapse_reverse_proxy_companion_synapse_cache_max_size_mb }}m;
{% endif %}
# Round Robin "upstream" pools for workers
{% if room_workers | length > 0 %}
{{- render_worker_upstream('room_workers_upstream', room_workers, 'hash $room_name consistent;') }}
{% endif %}
{% if sync_workers | length > 0 %}
{{- render_worker_upstream('sync_workers_upstream', sync_workers, 'hash $user_identifier consistent;') }}
{% endif %}
{% if client_reader_workers | length > 0 %}
{{- render_worker_upstream('client_reader_workers_upstream', client_reader_workers, 'least_conn;') }}
{% endif %}
{% if federation_reader_workers | length > 0 %}
{{- render_worker_upstream('federation_reader_workers_upstream', federation_reader_workers, 'hash $http_x_forwarded_for;') }}
{% endif %}
{% if generic_workers | length > 0 %}
{{- render_worker_upstream('generic_workers_upstream', generic_workers, 'hash $http_x_forwarded_for;') }}
{% endif %}
{% if stream_writer_typing_stream_workers | length > 0 %}
{{- render_worker_upstream('stream_writer_typing_stream_workers_upstream', stream_writer_typing_stream_workers, '') }}
{% endif %}
{% if stream_writer_to_device_stream_workers | length > 0 %}
{{- render_worker_upstream('stream_writer_to_device_stream_workers_upstream', stream_writer_to_device_stream_workers, '') }}
{% endif %}
{% if stream_writer_account_data_stream_workers | length > 0 %}
{{- render_worker_upstream('stream_writer_account_data_stream_workers_upstream', stream_writer_account_data_stream_workers, '') }}
{% endif %}
{% if stream_writer_receipts_stream_workers | length > 0 %}
{{- render_worker_upstream('stream_writer_receipts_stream_workers_upstream', stream_writer_receipts_stream_workers, '') }}
{% endif %}
{% if stream_writer_presence_stream_workers | length > 0 %}
{{- render_worker_upstream('stream_writer_presence_stream_workers_upstream', stream_writer_presence_stream_workers, '') }}
{% endif %}
{% if media_repository_workers | length > 0 %}
{{- render_worker_upstream('media_repository_workers_upstream', media_repository_workers, 'least_conn;') }}
{% endif %}
{% if user_dir_workers | length > 0 %}
{{- render_worker_upstream('user_dir_workers_upstream', user_dir_workers, '') }}
{% endif %}
{% endif %}
server {
listen 8008;
server_name {{ matrix_synapse_reverse_proxy_companion_hostname }};
server_tokens off;
root /dev/null;
client_max_body_size {{ matrix_synapse_reverse_proxy_companion_client_api_client_max_body_size_mb }}M;
client_body_buffer_size {{ matrix_synapse_reverse_proxy_companion_client_api_client_body_buffer_size_mb }}M;
proxy_buffering on;
proxy_max_temp_file_size 0;
proxy_set_header Host $host;
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled %}
# Internal location for whoami-based sync worker routing.
# This is called via auth_request from sync worker locations.
# The njs handler calls the whoami endpoint to resolve access tokens to usernames,
# then returns the username in the X-User-Identifier header for upstream hashing.
location = /_whoami_sync_worker_router {
internal;
js_content whoami_sync_worker_router.handleAuthRequest;
}
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_synapse_workers_enabled %}
# Client-server overrides — These locations must go to the main Synapse process
location ~ {{ matrix_synapse_reverse_proxy_companion_client_server_main_override_locations_regex }} {
{# FIXME: This block was copied from the main Synapse fallback below. It would be better to have it in one place and avoid duplication. #}
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_client_api_addr }}";
proxy_pass http://$backend;
}
# Client-server SSO overrides — These locations must go to the main Synapse process
location ~ {{ matrix_synapse_reverse_proxy_companion_client_server_sso_override_locations_regex }} {
{# FIXME: This block was copied from the main Synapse fallback below. It would be better to have it in one place and avoid duplication. #}
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_client_api_addr }}";
proxy_pass http://$backend;
}
# QR code login (`rendezvous`) locations need to go to the same Synapse process.
# It doesn't necessarily need to be the main process, but it needs to be consistent.
# For simplicity, we'll send them to the main process though.
location ~ {{ matrix_synapse_reverse_proxy_companion_client_server_qr_code_login_locations_regex }} {
{# FIXME: This block was copied from the main Synapse fallback below. It would be better to have it in one place and avoid duplication. #}
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_client_api_addr }}";
proxy_pass http://$backend;
}
{# Workers redirects BEGIN #}
{% if generic_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#synapseappgeneric_worker
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_generic_worker_client_server_locations, 'generic_workers_upstream') }}
{% endif %}
{% if stream_writer_typing_stream_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#the-typing-stream
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_stream_writer_typing_stream_worker_client_server_locations, 'stream_writer_typing_stream_workers_upstream') }}
{% endif %}
{% if stream_writer_to_device_stream_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#the-to_device-stream
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_stream_writer_to_device_stream_worker_client_server_locations, 'stream_writer_to_device_stream_workers_upstream') }}
{% endif %}
{% if stream_writer_account_data_stream_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#the-account_data-stream
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_stream_writer_account_data_stream_worker_client_server_locations, 'stream_writer_account_data_stream_workers_upstream') }}
{% endif %}
{% if stream_writer_receipts_stream_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#the-receipts-stream
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_stream_writer_receipts_stream_worker_client_server_locations, 'stream_writer_receipts_stream_workers_upstream') }}
{% endif %}
{% if stream_writer_presence_stream_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#the-presence-stream
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_stream_writer_presence_stream_worker_client_server_locations, 'stream_writer_presence_stream_workers_upstream') }}
{% endif %}
{% if room_workers | length > 0 %}
# room workers
# https://tcpipuk.github.io/synapse/deployment/workers.html
# https://tcpipuk.github.io/synapse/deployment/nginx.html#locationsconf
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_room_worker_client_server_locations, 'room_workers_upstream') }}
{% endif %}
{% if sync_workers | length > 0 %}
# sync workers
# https://tcpipuk.github.io/synapse/deployment/workers.html
# https://tcpipuk.github.io/synapse/deployment/nginx.html#locationsconf
{{ render_locations_to_upstream_with_whoami_sync_worker_router(matrix_synapse_reverse_proxy_companion_synapse_sync_worker_client_server_locations, 'sync_workers_upstream') }}
{% endif %}
{% if client_reader_workers | length > 0 %}
# client_reader workers
# https://tcpipuk.github.io/synapse/deployment/workers.html
# https://tcpipuk.github.io/synapse/deployment/nginx.html#locationsconf
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_client_reader_client_server_locations, 'client_reader_workers_upstream') }}
{% endif %}
{% if media_repository_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#synapseappmedia_repository
{% for location in matrix_synapse_reverse_proxy_companion_synapse_media_repository_locations %}
location ~ {{ location }} {
proxy_pass http://media_repository_workers_upstream$request_uri;
{% if matrix_synapse_reverse_proxy_companion_synapse_cache_enabled %}
proxy_cache {{ matrix_synapse_reverse_proxy_companion_synapse_cache_keys_zone_name }};
proxy_cache_valid any {{ matrix_synapse_reverse_proxy_companion_synapse_cache_proxy_cache_valid_time }};
proxy_force_ranges on;
add_header X-Cache-Status $upstream_cache_status;
{% endif %}
}
{% endfor %}
{% endif %}
{% if user_dir_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#updating-the-user-directory
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_user_dir_locations, 'user_dir_workers_upstream') }}
{% endif %}
{# Workers redirects END #}
{% endif %}
{% for configuration_block in matrix_synapse_reverse_proxy_companion_synapse_client_api_additional_server_configuration_blocks %}
{{- configuration_block }}
{% endfor %}
{# Everything else just goes to the API server ##}
location / {
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_client_api_addr }}";
proxy_pass http://$backend;
}
}
{% if matrix_synapse_reverse_proxy_companion_federation_api_enabled %}
server {
listen 8048;
server_name {{ matrix_synapse_reverse_proxy_companion_hostname }};
server_tokens off;
root /dev/null;
client_max_body_size {{ matrix_synapse_reverse_proxy_companion_federation_api_client_max_body_size_mb }}M;
client_body_buffer_size {{ matrix_synapse_reverse_proxy_companion_federation_api_client_body_buffer_size_mb }}M;
proxy_buffering on;
proxy_max_temp_file_size 0;
proxy_set_header Host $host;
{% if matrix_synapse_reverse_proxy_companion_synapse_workers_enabled %}
# Federation overrides — These locations must go to the main Synapse process
location ~ {{ matrix_synapse_reverse_proxy_companion_federation_override_locations_regex }} {
{# FIXME: This block was copied from the fallback location below. It would be better to have it in one place and avoid duplication. #}
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_federation_api_addr }}";
proxy_pass http://$backend;
}
{% if room_workers | length > 0 %}
# https://tcpipuk.github.io/synapse/deployment/workers.html
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_room_worker_federation_locations, 'room_workers_upstream') }}
{% endif %}
{% if generic_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#synapseappgeneric_worker
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_generic_worker_federation_locations, 'generic_workers_upstream') }}
{% endif %}
{% if media_repository_workers | length > 0 %}
# https://matrix-org.github.io/synapse/latest/workers.html#synapseappmedia_repository
{% for location in matrix_synapse_reverse_proxy_companion_synapse_media_repository_locations %}
location ~ {{ location }} {
proxy_pass http://media_repository_workers_upstream$request_uri;
{% if matrix_synapse_reverse_proxy_companion_synapse_cache_enabled %}
proxy_buffering on;
proxy_cache {{ matrix_synapse_reverse_proxy_companion_synapse_cache_keys_zone_name }};
proxy_cache_valid any {{ matrix_synapse_reverse_proxy_companion_synapse_cache_proxy_cache_valid_time }};
proxy_force_ranges on;
add_header X-Cache-Status $upstream_cache_status;
{% endif %}
}
{% endfor %}
{% endif %}
{#
This is last, because we'd like more-specific requests (e.g. `/_matrix/federation/v1/media/` that may be handled by a media repository worker, if enabled)
to be routed to more specialized workers via their respective `locations` defined earlier (above).
As https://nginx.org/en/docs/http/ngx_http_core_module.html#location says about location matching:
> .. Then regular expressions are checked, in the order of their appearance in the configuration file.
See: https://github.com/spantaleev/matrix-docker-ansible-deploy/issues/3918
#}
{% if federation_reader_workers | length > 0 %}
# https://tcpipuk.github.io/synapse/deployment/workers.html
{{ render_locations_to_upstream(matrix_synapse_reverse_proxy_companion_synapse_federation_reader_federation_locations, 'federation_reader_workers_upstream') }}
{% endif %}
{% endif %}
{% for configuration_block in matrix_synapse_reverse_proxy_companion_synapse_federation_api_additional_server_configuration_blocks %}
{{- configuration_block }}
{% endfor %}
location / {
{# Use the embedded DNS resolver in Docker containers to discover the service #}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }} valid=5s;
set $backend "{{ matrix_synapse_reverse_proxy_companion_federation_api_addr }}";
proxy_pass http://$backend;
}
}
{% endif %}

View File

@@ -0,0 +1,5 @@
SPDX-FileCopyrightText: 2022 - 2025 Slavi Pantaleev
SPDX-FileCopyrightText: 2024 Charles Wright
SPDX-FileCopyrightText: 2024 Suguru Hirahara
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@@ -0,0 +1,13 @@
#jinja2: lstrip_blocks: True
# The default is aligned to the CPU's cache size,
# which can sometimes be too low.
# Thus, we ensure a larger bucket size value is used.
server_names_hash_bucket_size 64;
{% if matrix_synapse_reverse_proxy_companion_http_level_resolver %}
resolver {{ matrix_synapse_reverse_proxy_companion_http_level_resolver }};
{% endif %}
{% for configuration_block in matrix_synapse_reverse_proxy_companion_http_additional_server_configuration_blocks %}
{{- configuration_block }}
{% endfor %}

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Slavi Pantaleev
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@@ -0,0 +1,93 @@
#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
# load_module directives must be first or nginx will choke with:
# > [emerg] "load_module" directive is specified too late.
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
load_module modules/ngx_http_js_module.so;
{% endif %}
worker_processes {{ matrix_synapse_reverse_proxy_companion_worker_processes }};
error_log /var/log/nginx/error.log warn;
pid /tmp/nginx.pid;
{% for configuration_block in matrix_synapse_reverse_proxy_companion_additional_configuration_blocks %}
{{- configuration_block }}
{% endfor %}
events {
worker_connections {{ matrix_synapse_reverse_proxy_companion_worker_connections }};
{% for configuration_block in matrix_synapse_reverse_proxy_companion_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;
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
js_path /njs/;
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_enabled %}
# njs module for whoami-based sync worker routing
js_import whoami_sync_worker_router from whoami_sync_worker_router.js;
js_shared_dict_zone zone=whoami_sync_worker_router_cache:{{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_size_mb }}m;
{% endif %}
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_synapse_reverse_proxy_companion_access_log_enabled %}
access_log /var/log/nginx/access.log main;
{% else %}
access_log off;
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_access_log_syslog_integration_enabled %}
log_format prometheus_fmt 'matrix-synapse-reverse-proxy-companion $server_name - $upstream_addr - $remote_addr - $remote_user [$time_local] '
'$host "$request" '
'$status "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log syslog:server={{ matrix_synapse_reverse_proxy_companion_access_log_syslog_integration_server_port }},tag={{ matrix_synapse_reverse_proxy_companion_access_log_syslog_integration_tag }} prometheus_fmt;
{% endif %}
{% if not matrix_synapse_reverse_proxy_companion_access_log_enabled and not matrix_synapse_reverse_proxy_companion_access_log_syslog_integration_enabled %}
access_log off;
{% endif %}
proxy_connect_timeout {{ matrix_synapse_reverse_proxy_companion_proxy_connect_timeout }};
proxy_send_timeout {{ matrix_synapse_reverse_proxy_companion_proxy_send_timeout }};
proxy_read_timeout {{ matrix_synapse_reverse_proxy_companion_proxy_read_timeout }};
send_timeout {{ matrix_synapse_reverse_proxy_companion_send_timeout }};
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
server_tokens off;
{# Map directive needed for proxied WebSocket upgrades #}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2022 - 2024 Slavi Pantaleev
SPDX-FileCopyrightText: 2022 MDAD project contributors
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@@ -0,0 +1,202 @@
#jinja2: lstrip_blocks: True
// Whoami-based sync worker router
//
// This script resolves access tokens to usernames by calling the whoami endpoint.
// Results are cached to minimize latency impact. The username is returned via the
// X-User-Identifier header, which nginx captures and uses for upstream hashing.
//
// This works with any authentication system (native Synapse auth, MAS, etc.) because
// Synapse handles token validation internally.
//
// Why auth_request instead of js_set?
// -----------------------------------
// A simpler approach would be to use js_set to populate a variable (e.g., $user_identifier)
// and then use that variable in an upstream's `hash` directive. However, this doesn't work
// because:
//
// 1. The whoami lookup requires an HTTP subrequest (ngx.fetch), which is asynchronous.
// 2. js_set handlers must return synchronously - nginx's variable evaluation doesn't support
// async operations. Using async functions with js_set causes errors like:
// "async operation inside variable handler"
//
// The auth_request approach solves this by:
// 1. Making a subrequest to an internal location that uses js_content (which supports async)
// 2. Returning the user identifier via a response header (X-User-Identifier)
// 3. Capturing that header with auth_request_set into $user_identifier
// 4. Using $user_identifier in the upstream's hash directive for consistent routing
const WHOAMI_URL = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_url | to_json }};
const CACHE_TTL_MS = {{ (matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_cache_ttl_seconds * 1000) | to_json }};
const LOGGING_ENABLED = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_enabled | to_json }};
const LOGGING_TOKEN_LENGTH = {{ matrix_synapse_reverse_proxy_companion_whoami_sync_worker_router_logging_token_length | to_json }};
function log(message) {
if (LOGGING_ENABLED) {
// Using WARN level because nginx's error_log is hardcoded to 'warn' and our logs won't be visible otherwise
ngx.log(ngx.WARN, 'whoami_sync_worker_router: ' + message);
}
}
// Truncate token for logging (show first X chars only for security)
function truncateToken(token) {
if (!token || token.length <= LOGGING_TOKEN_LENGTH) {
return token;
}
return token.substring(0, LOGGING_TOKEN_LENGTH) + '...';
}
// Extract token from request (Authorization header or query parameter)
function extractToken(r) {
// Try Authorization header first
const authHeader = r.headersIn['Authorization'];
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7);
}
// Fall back to access_token query parameter (deprecated in Matrix v1.11, but homeservers must support it)
if (r.args.access_token) {
return r.args.access_token;
}
return null;
}
// Extract localpart from user_id (e.g., "@alice:example.com" -> "alice")
function extractLocalpart(userId) {
if (!userId || !userId.startsWith('@')) {
return null;
}
const colonIndex = userId.indexOf(':');
if (colonIndex === -1) {
return null;
}
return userId.substring(1, colonIndex);
}
// Get cached username for token
function getCachedUsername(token) {
const cache = ngx.shared.whoami_sync_worker_router_cache;
if (!cache) {
return null;
}
const entry = cache.get(token);
if (entry) {
try {
const data = JSON.parse(entry);
if (data.expires > Date.now()) {
log('cache hit for token ' + truncateToken(token) + ' -> ' + data.username);
return data.username;
}
// Expired, remove from cache
log('cache expired for token ' + truncateToken(token));
cache.delete(token);
} catch (e) {
cache.delete(token);
}
}
return null;
}
// Cache username for token
function cacheUsername(token, username) {
const cache = ngx.shared.whoami_sync_worker_router_cache;
if (!cache) {
return;
}
try {
const entry = JSON.stringify({
username: username,
expires: Date.now() + CACHE_TTL_MS
});
cache.set(token, entry);
log('cached token ' + truncateToken(token) + ' -> ' + username);
} catch (e) {
// Cache full or other error, log and continue
ngx.log(ngx.WARN, 'whoami_sync_worker_router: cache error: ' + e.message);
}
}
// Call whoami endpoint to get user_id
async function lookupWhoami(token) {
log('performing whoami lookup for token ' + truncateToken(token));
try {
const response = await ngx.fetch(WHOAMI_URL, {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
if (response.ok) {
const data = await response.json();
if (data.user_id) {
const localpart = extractLocalpart(data.user_id);
log('whoami lookup success: ' + data.user_id + ' -> ' + localpart);
return localpart;
}
} else if (response.status === 401) {
// Token is invalid/expired - this is expected for some requests
log('whoami lookup returned 401 (invalid/expired token)');
return null;
} else {
ngx.log(ngx.WARN, 'whoami_sync_worker_router: whoami returned status ' + response.status);
}
} catch (e) {
ngx.log(ngx.ERR, 'whoami_sync_worker_router: whoami failed: ' + e.message);
}
return null;
}
// Set response header with the user identifier for upstream hashing
function setUserIdentifier(r, identifier) {
log('resolved user identifier: ' + identifier);
r.headersOut['X-User-Identifier'] = identifier;
}
// Main handler for auth_request subrequest.
// Returns 200 with X-User-Identifier header containing the user identifier for upstream hashing.
async function handleAuthRequest(r) {
const token = extractToken(r);
if (!token) {
// No token found (e.g., OPTIONS preflight requests don't include Authorization header).
// We return a random value to distribute these requests across workers.
// Returning an empty string would cause all no-token requests to hash to the same value,
// routing them all to a single worker.
// This doesn't affect the cache since we only cache token -> username mappings.
log('no token found in request, distributing randomly');
setUserIdentifier(r, '_no_token_' + Math.random());
r.return(200);
return;
}
// Check cache first
const cachedUsername = getCachedUsername(token);
if (cachedUsername) {
setUserIdentifier(r, cachedUsername);
r.return(200);
return;
}
// Perform whoami lookup
log('cache miss for token ' + truncateToken(token));
const username = await lookupWhoami(token);
if (username) {
cacheUsername(token, username);
setUserIdentifier(r, username);
r.return(200);
return;
}
// Whoami lookup failed, fall back to using the token itself for hashing.
// This still provides device-level sticky routing (same token -> same worker).
log('whoami lookup failed, falling back to token-based routing');
setUserIdentifier(r, token);
r.return(200);
}
export default { handleAuthRequest };

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 Slavi Pantaleev
SPDX-License-Identifier: AGPL-3.0-or-later

View File

@@ -0,0 +1,62 @@
#jinja2: lstrip_blocks: True
[Unit]
Description=Synapse reverse-proxy companion
{% for service in matrix_synapse_reverse_proxy_companion_systemd_required_services_list %}
Requires={{ service }}
After={{ service }}
{% endfor %}
{% for service in matrix_synapse_reverse_proxy_companion_systemd_wanted_services_list %}
Wants={{ service }}
{% endfor %}
DefaultDependencies=no
[Service]
Type=simple
Environment="HOME={{ devture_systemd_docker_base_systemd_unit_home_path }}"
ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-synapse-reverse-proxy-companion 2>/dev/null || true'
ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-synapse-reverse-proxy-companion 2>/dev/null || true'
ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \
--rm \
--name=matrix-synapse-reverse-proxy-companion \
--log-driver=none \
--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
--cap-drop=ALL \
--read-only \
--tmpfs=/tmp:rw,noexec,nosuid,size={{ matrix_synapse_reverse_proxy_companion_tmp_directory_size_mb }}m \
{% if matrix_synapse_reverse_proxy_companion_synapse_cache_enabled %}
--tmpfs=/tmp/synapse-cache:rw,noexec,nosuid,size={{ matrix_synapse_reverse_proxy_companion_tmp_cache_directory_size_mb }}m\
{% endif %}
--network={{ matrix_synapse_reverse_proxy_companion_container_network }} \
{% if matrix_synapse_reverse_proxy_companion_container_client_api_host_bind_port %}
-p {{ matrix_synapse_reverse_proxy_companion_container_client_api_host_bind_port }}:8008 \
{% endif %}
{% if matrix_synapse_reverse_proxy_companion_container_federation_api_host_bind_port %}
-p {{ matrix_synapse_reverse_proxy_companion_container_federation_api_host_bind_port }}:8048 \
{% endif %}
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_base_path }}/nginx.conf,dst=/etc/nginx/nginx.conf,ro \
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_confd_path }},dst=/etc/nginx/conf.d,ro \
{% if matrix_synapse_reverse_proxy_companion_njs_enabled %}
--mount type=bind,src={{ matrix_synapse_reverse_proxy_companion_njs_path }},dst=/njs,ro \
{% endif %}
--label-file={{ matrix_synapse_reverse_proxy_companion_base_path }}/labels \
{% for arg in matrix_synapse_reverse_proxy_companion_container_arguments %}
{{ arg }} \
{% endfor %}
{{ matrix_synapse_reverse_proxy_companion_container_image }}
{% for network in matrix_synapse_reverse_proxy_companion_container_additional_networks %}
ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} network connect {{ network }} matrix-synapse-reverse-proxy-companion
{% endfor %}
ExecStart={{ devture_systemd_docker_base_host_command_docker }} start --attach matrix-synapse-reverse-proxy-companion
ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-synapse-reverse-proxy-companion 2>/dev/null || true'
ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-synapse-reverse-proxy-companion 2>/dev/null || true'
ExecReload={{ devture_systemd_docker_base_host_command_docker }} exec matrix-synapse-reverse-proxy-companion /usr/sbin/nginx -s reload
Restart=always
RestartSec=30
SyslogIdentifier=matrix-synapse-reverse-proxy-companion
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2022 - 2024 Slavi Pantaleev
SPDX-FileCopyrightText: 2024 Michael Hollister
SPDX-License-Identifier: AGPL-3.0-or-later