# ============================================================================== # Matrix Synapse Stack — VM-102 (10.250.50.70) # Domain: matrix.phasefinal.com # # Deploy: /opt/docker/compose/synapse/compose.yaml # Config: /opt/docker/conf/synapse/homeserver.yaml # # Services: # 1. synapse-db — Postgres 16 (internal only) # 2. synapse — Matrix Synapse homeserver (port 8008) # 3. element-web — Element Web client (port 8080) # # Stack uses traefik-net (tnet) for reverse proxy / TLS termination. # Cert resolver: anaprod (matches existing VM-102 convention) # ============================================================================== services: # --------------------------------------------------------------------------- # Postgres database for Synapse # --------------------------------------------------------------------------- synapse-db: image: postgres:16-alpine container_name: synapse-db restart: unless-stopped environment: POSTGRES_DB: synapse POSTGRES_USER: synapse POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} POSTGRES_INITDB_ARGS: --lc-collate=C --lc-ctype=C --encoding=UTF8 volumes: - synapse-db-data:/var/lib/postgresql/data networks: - synapse-internal healthcheck: test: - CMD-SHELL - pg_isready -U synapse -d synapse interval: 10s timeout: 5s retries: 5 # --------------------------------------------------------------------------- # Matrix Synapse homeserver # --------------------------------------------------------------------------- synapse: image: matrixdotorg/synapse:v1.159.0 container_name: synapse restart: unless-stopped depends_on: synapse-db: condition: service_healthy volumes: - /opt/docker/conf/synapse/homeserver.yaml:/data/homeserver.yaml:ro - synapse-data:/data - /opt/docker/conf/synapse/aipa_appservice.yaml:/conf/aipa_appservice.yaml:ro networks: - tnet - synapse-internal labels: - traefik.enable=true - traefik.http.routers.synapse.rule=Host(`matrix.phasefinal.com`) - traefik.http.routers.synapse.tls=true - traefik.http.routers.synapse.tls.certresolver=anaprod - traefik.http.services.synapse.loadbalancer.server.port=8008 # -- /_synapse/admin is LAN-only ------------------------------------ # Synapse mounts its admin API on the same vhost as the client API, so # publishing matrix.phasefinal.com published the admin surface too. It # answered 200 from the open internet. HMAC-protected, so not an open # door -- but Synapse's own guidance is to keep it off the public # listener, and a shared secret is a poor last line. # # Longer rule than the parent router; priority set EXPLICITLY rather than # relying on Traefik's rule-length tie-break. # # This host is NOT Cloudflare-proxied (plain A record to 38.120.12.44), # so the address Traefik sees is the real client and there is no # forwarded-header to spoof past. # # WARNING: if this record is ever put behind Cloudflare, every request # arrives from a Cloudflare address and matches nothing here. That is # deny-everyone, and it tells you nothing about why. Revisit if proxied. # # ⚠ AS OF 2026-09-01 THE 10.0.0.0/8 ENTRY MATCHES NOTHING, AND THAT IS # EXPECTED. matrix.phasefinal.com resolves PUBLICLY, so fleet hosts # reaching it go out their own WAN and arrive here as a public address -- # measured: a request from nh3-dev (10.100.10.50) landed as 70.230.226.88. # So this rule is EFFECTIVELY DENY-ALL through Traefik, which is the # intended posture, not an accident: # # - Admin work is done via `docker exec synapse` against localhost:8008, # which never traverses Traefik and is unaffected by any of this. # - Allow-listing the sites' WAN addresses was considered and REJECTED. # They are dynamic; a stale entry either locks us out or, worse, hands # admin to whoever inherits the address next. # # The private ranges are kept anyway because they are free and become live # the moment an internal DNS rewrite points this name at 10.250.50.70 -- # at which point fleet traffic stops hairpinning and starts matching. - "traefik.http.routers.synapse-admin.rule=Host(`matrix.phasefinal.com`) && PathPrefix(`/_synapse/admin`)" - traefik.http.routers.synapse-admin.priority=100 - traefik.http.routers.synapse-admin.entrypoints=websecure - traefik.http.routers.synapse-admin.tls=true - traefik.http.routers.synapse-admin.tls.certresolver=anaprod - traefik.http.routers.synapse-admin.service=synapse - traefik.http.routers.synapse-admin.middlewares=synapse-admin-lan # Traefik v3 spelling: ipallowlist. (v2 called it ipwhitelist.) - traefik.http.middlewares.synapse-admin-lan.ipallowlist.sourcerange=10.0.0.0/8,127.0.0.1/32 # --------------------------------------------------------------------------- # Element Web client # --------------------------------------------------------------------------- element-web: image: vectorim/element-web:v1.12.27 container_name: element-web restart: unless-stopped depends_on: - synapse volumes: - /opt/docker/conf/synapse/element-config.json:/app/config.json:ro networks: - tnet labels: - traefik.enable=true - traefik.http.routers.element.rule=Host(`chat.phasefinal.com`) - traefik.http.routers.element.tls=true - traefik.http.routers.element.tls.certresolver=anaprod - traefik.http.services.element.loadbalancer.server.port=80 - homepage.group=Apps - homepage.name=Matrix (Element) - homepage.icon=si-element - homepage.description=Matrix homeserver + Element Web client - homepage.href=https://chat.phasefinal.com networks: synapse-internal: driver: bridge tnet: name: traefik-net external: true # ============================================================================= # Volumes # ============================================================================= volumes: synapse-db-data: null synapse-data: null