# {{ ansible_managed }} # ESS Pro v{{ ess_chart_version }} on docker compose — rendered by ess_pro_compose. # Topology mirrors the Helm chart: HAProxy fronts all Synapse traffic, # synapse-main is the Python homeserver, synapse-fed-reader is the Rust Pro # worker handling federation reads, MAS handles all auth, LiveKit + lk-jwt # serve Element Call. name: {{ ess_compose_project_name }} networks: {{ ess_compose_traefik_network }}: external: true {{ ess_compose_internal_network }}: driver: bridge volumes: postgres_data: synapse_media: services: # =========================================================================== # Data plane # =========================================================================== postgres: image: {{ ess_images.postgres }} container_name: postgres restart: unless-stopped networks: [ {{ ess_compose_internal_network }} ] environment: LC_COLLATE: "C" LC_CTYPE: "C" PGDATA: /var/lib/postgresql/data/pgdata POSTGRES_INITDB_ARGS: "-E UTF8" POSTGRES_PASSWORD_FILE: /secrets/ess-generated/POSTGRES_ADMIN_PASSWORD command: - postgres - "-c" - "max_connections={{ ess_postgres_max_connections }}" - "-c" - "shared_buffers={{ ess_postgres_shared_buffers }}" - "-c" - "effective_cache_size={{ ess_postgres_effective_cache_size }}" volumes: - postgres_data:/var/lib/postgresql/data - {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro - {{ ess_compose_conf_dir }}/postgres/configure-dbs.sh:/docker-entrypoint-initdb.d/init-ess-dbs.sh:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 10 redis: image: {{ ess_images.redis }} container_name: redis restart: unless-stopped networks: [ {{ ess_compose_internal_network }} ] command: ["/usr/local/etc/redis/redis.conf"] volumes: - {{ ess_compose_conf_dir }}/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 # =========================================================================== # Synapse (Python main + Rust federation-reader worker) # =========================================================================== synapse-main: image: {{ ess_images.synapse }} container_name: synapse-main restart: unless-stopped depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } networks: [ {{ ess_compose_internal_network }} ] command: ["python3", "-m", "synapse.app.homeserver", "-c", "/conf/homeserver.yaml"] volumes: - {{ ess_compose_conf_dir }}/synapse/homeserver.yaml:/conf/homeserver.yaml:ro - {{ ess_compose_conf_dir }}/synapse/log_config.yaml:/conf/log_config.yaml:ro - {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro - synapse_media:/media healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] interval: 10s timeout: 5s retries: 30 start_period: 60s {% for i in range(ess_synapse_fed_reader_replicas | int) %} synapse-fed-reader-{{ i }}: image: {{ ess_images.synapse_pro_worker }} container_name: synapse-fed-reader-{{ i }} restart: unless-stopped depends_on: synapse-main: { condition: service_healthy } networks: [ {{ ess_compose_internal_network }} ] environment: APP_CONFIG_FILEPATH: /conf/federation-reader.yaml volumes: - {{ ess_compose_conf_dir }}/synapse/federation-reader.yaml:/conf/federation-reader.yaml:ro - {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro {% endfor %} # =========================================================================== # Matrix Authentication Service (4 listeners) # =========================================================================== mas: image: {{ ess_images.mas }} container_name: mas restart: unless-stopped depends_on: postgres: { condition: service_healthy } networks: - {{ ess_compose_internal_network }} - {{ ess_compose_traefik_network }} environment: MAS_CONFIG: /conf/mas-config.yaml command: ["server", "--no-migrate"] volumes: - {{ ess_compose_conf_dir }}/mas/config.yaml:/conf/mas-config.yaml:ro - {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost:8081/health"] interval: 10s timeout: 5s retries: 20 start_period: 30s labels: - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-mas.rule=Host(`{{ ess_hostnames.mas }}`)" - "traefik.http.routers.ess-mas.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-mas.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-mas.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.services.ess-mas.loadbalancer.server.port=8080" # MAS root listener (port 8082) is mounted as a separate Traefik router so # /.well-known/openid-configuration on the apex of the mas host is reachable. # We attach a second router on the same service via a path rule. # =========================================================================== # HAProxy — fronts all Synapse + well-known traffic # =========================================================================== haproxy: image: {{ ess_images.haproxy }} container_name: haproxy restart: unless-stopped depends_on: synapse-main: { condition: service_healthy } networks: - {{ ess_compose_internal_network }} - {{ ess_compose_traefik_network }} command: ["-f", "/usr/local/etc/haproxy/haproxy.cfg", "-dW"] volumes: - {{ ess_compose_conf_dir }}/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - {{ ess_compose_conf_dir }}/haproxy/path_map_file:/synapse/path_map_file:ro - {{ ess_compose_conf_dir }}/haproxy/path_map_file_get:/synapse/path_map_file_get:ro - {{ ess_compose_conf_dir }}/haproxy/429.http:/synapse/429.http:ro - {{ ess_compose_conf_dir }}/haproxy/admin-allow-ips.lst:/synapse/admin-allow-ips.lst:ro - {{ ess_compose_conf_dir }}/haproxy/well-known/server:/well-known/server:ro - {{ ess_compose_conf_dir }}/haproxy/well-known/client:/well-known/client:ro - {{ ess_compose_conf_dir }}/haproxy/well-known/support:/well-known/support:ro - {{ ess_compose_conf_dir }}/haproxy/well-known/element.json:/well-known/element.json:ro healthcheck: test: ["CMD", "wget", "-q", "-O-", "http://localhost:8406/synapse_ready"] interval: 15s timeout: 5s retries: 20 start_period: 90s labels: # matrix. -> HAProxy frontend synapse-http-in (port 8008) - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-synapse.rule=Host(`{{ ess_hostnames.synapse }}`)" - "traefik.http.routers.ess-synapse.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-synapse.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-synapse.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.routers.ess-synapse.service=ess-synapse" - "traefik.http.services.ess-synapse.loadbalancer.server.port=8008" # /.well-known/matrix -> HAProxy well-known-in (port 8010) - "traefik.http.routers.ess-wellknown.rule=Host(`{{ ess_server_name }}`) && PathPrefix(`/.well-known/matrix`)" - "traefik.http.routers.ess-wellknown.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-wellknown.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-wellknown.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.routers.ess-wellknown.service=ess-wellknown" - "traefik.http.services.ess-wellknown.loadbalancer.server.port=8010" # =========================================================================== # Element Web (browser client) # =========================================================================== element-web: image: {{ ess_images.element_web }} container_name: element-web restart: unless-stopped networks: [ {{ ess_compose_traefik_network }} ] volumes: - {{ ess_compose_conf_dir }}/element-web/config.json:/app/config.json:ro labels: - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-element-web.rule=Host(`{{ ess_hostnames.element_web }}`)" - "traefik.http.routers.ess-element-web.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-element-web.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-element-web.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.services.ess-element-web.loadbalancer.server.port=8080" # =========================================================================== # Element Admin (admin panel) # =========================================================================== element-admin: image: {{ ess_images.element_admin }} container_name: element-admin restart: unless-stopped networks: [ {{ ess_compose_traefik_network }} ] environment: SERVER_NAME: "{{ ess_server_name }}" labels: - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-element-admin.rule=Host(`{{ ess_hostnames.element_admin }}`)" - "traefik.http.routers.ess-element-admin.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-element-admin.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-element-admin.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.services.ess-element-admin.loadbalancer.server.port=8080" # =========================================================================== # Matrix RTC / Element Call (LiveKit SFU + lk-jwt) # =========================================================================== matrix-rtc-sfu: image: {{ ess_images.livekit }} container_name: matrix-rtc-sfu restart: unless-stopped networks: - {{ ess_compose_internal_network }} - {{ ess_compose_traefik_network }} command: ["--config", "/conf/sfu-config.yaml"] volumes: - {{ ess_compose_conf_dir }}/sfu/config.yaml:/conf/sfu-config.yaml:ro # WebRTC media ports — DMZ firewall must NAT-forward these to this host. ports: - "{{ ess_rtc_tcp_port }}:{{ ess_rtc_tcp_port }}/tcp" - "{{ ess_rtc_udp_port }}:{{ ess_rtc_udp_port }}/udp" labels: - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-matrix-rtc.rule=Host(`{{ ess_hostnames.matrix_rtc }}`)" - "traefik.http.routers.ess-matrix-rtc.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-matrix-rtc.tls=true" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-matrix-rtc.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.routers.ess-matrix-rtc.service=ess-matrix-rtc" - "traefik.http.services.ess-matrix-rtc.loadbalancer.server.port=7880" matrix-rtc-authorisation: image: {{ ess_images.lk_jwt }} container_name: matrix-rtc-authorisation restart: unless-stopped depends_on: matrix-rtc-sfu: { condition: service_started } networks: - {{ ess_compose_internal_network }} - {{ ess_compose_traefik_network }} environment: LIVEKIT_URL: "wss://{{ ess_hostnames.matrix_rtc }}" LIVEKIT_KEY: "{{ ess_livekit_key }}" LIVEKIT_SECRET_FROM_FILE: /secrets/ess-generated/ELEMENT_CALL_LIVEKIT_SECRET LIVEKIT_FULL_ACCESS_HOMESERVERS: "{{ ess_server_name }}" volumes: - {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro labels: # /sfu/get is the JWT token endpoint Element Call hits to join calls. # It lives on the same host as the SFU but on a different backend. - "traefik.enable=true" - "traefik.docker.network={{ ess_compose_traefik_network }}" - "traefik.http.routers.ess-matrix-rtc-auth.rule=Host(`{{ ess_hostnames.matrix_rtc }}`) && PathPrefix(`/sfu/get`)" - "traefik.http.routers.ess-matrix-rtc-auth.entrypoints={{ ess_compose_traefik_entrypoint }}" - "traefik.http.routers.ess-matrix-rtc-auth.tls=true" - "traefik.http.routers.ess-matrix-rtc-auth.priority=200" {% if ess_compose_traefik_certresolver | length > 0 %} - "traefik.http.routers.ess-matrix-rtc-auth.tls.certresolver={{ ess_compose_traefik_certresolver }}" {% endif %} - "traefik.http.routers.ess-matrix-rtc-auth.service=ess-matrix-rtc-auth" - "traefik.http.services.ess-matrix-rtc-auth.loadbalancer.server.port=8080"