Skip to content

Setup Matrix Synapse (continues #153) - #202

Draft
miharp wants to merge 21 commits into
voxpupuli:productionfrom
miharp:matrix-synapse
Draft

Setup Matrix Synapse (continues #153)#202
miharp wants to merge 21 commits into
voxpupuli:productionfrom
miharp:matrix-synapse

Conversation

@miharp

@miharp miharp commented Aug 14, 2026

Copy link
Copy Markdown

Continues #153 by @genebean, who handed it over in #sig-matrix-ops. His commit is the base of this branch and the design decisions (docker-compose, the worker topology, S3 media) are his and unchanged. This adds the fixes needed to make the role deploy, found by running the controlrepo masterless in a throwaway Vagrant lab (miharp/matrix-lab) on a fresh Ubuntu 24.04 VM until Synapse served traffic.

Context: the puppet-nginx v7 to v8 major bump (#150) merged the day before the original branch was cut, and CI compiles no catalogs, so the typed-API breakage was invisible at review time.

19 fixes in 18 commits. The commit messages carry the per-fix reasoning; the short version:

  • 8 compile blockers: nginx v8 typed-API breakage, a duplicate Package[certbot], a lookup_options regex typo, docker::compose used as a defined type, profiles::docker missing from the role
  • Workers ran with no listeners: the official image ignores SYNAPSE_WORKER_* env vars, so each worker now gets its own config file carrying the replication listener that instance_map needs. This matches upstream's own contrib/docker_compose_workers example.
  • Ownership fixes so the container UID (991) can read its config and write its signing key
  • Docker networking: bridge pinned to matrix0 to match the nftables docker_ce parameters, a host egress rule, container_name pinned to the hostnames the config dials, per-worker healthchecks
  • Review feedback from @ananace applied: authenticated media routes, canonical sync endpoints, 600s sync read timeout
  • The well-known delegation files now carry their JSON, served with application/json and CORS (they were empty, which breaks federation for the whole server_name)
  • Base image pinned to Synapse v1.158.0 and pulled from ghcr.io/element-hq (no anonymous pull rate limit; :latest actually moved and broke the build mid-test)

Verified on a fresh Ubuntu 24.04 VM with zero manual steps (console log): full catalog applies, postgres/redis/main/4 workers/2 stream writers all healthy, /_matrix/client/versions serves through nginx. Lab caveats: ACME fails against fake domains, and S3 creds were dummies.

Also adds an rspec-puppet spec for profiles::matrix, closing the coverage gap that let the compile errors through.

Per the AI usage policy: I worked on this with Claude (Claude Code). Each commit carries the Co-authored-by trailer, and I have run and reviewed all of it myself in the lab above.

genebean and others added 21 commits April 10, 2026 00:00
This sets up a role and various profiles to run a Synapse server via
docker compose. The setup defines multiple workers so that it is
scalable and accounts for heavier than standard federation traffic
seeing as most accounts will originate on matrix.org instead of on this
Matrix servier.

Significant parts of this code were generated by Claude and via Google's
Antigravity IDE. In Claude's web interface, the Sonnet 4.6 model was
used. In Antigravity, the Gemini Pro 3.1 (High) model was used.
Attribution to both is provided both in this text and via Co-authored-by
entries in the commit.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Antigravity <antigravity@google.com>
The letsencrypt module (via profiles::lets_encrypt) already manages the
certbot package (aliased Package[letsencrypt] -> certbot) and renewal
via manage_cron, so including profiles::certbot alongside it fails
catalog compilation with a duplicate Package[certbot] declaration. No
other role combines the two profiles.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
The role never included profiles::docker, so nothing installed the
engine the compose stack needs. And docker::compose is a class in
puppetlabs-docker 10.x (it installs the compose plugin); the resource
for managing a stack is the docker_compose type.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
The nginx module types the parameter Optional[String[1]]; an empty
string fails compilation. The default behavior is what was wanted.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
The nginx module types members as Hash[String, Nginx::UpstreamMember];
plain address arrays fail compilation.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
…rings

The nginx module types proxy_set_header as Array[String]; a hash fails
compilation.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
The official image runs Synapse as UID 991 with /config mounted
read-only; a root-owned 0600 file gives PermissionError at startup and
every container crash-loops.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
homeserver.yaml sets log_config: /config/log.yaml, but nothing creates
it and /config is mounted read-only, so Synapse dies trying to generate
it. Console logging suits containerized workers.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
… UID

Docker auto-creates missing bind-mount sources root-owned; Synapse
(UID 991) then cannot write its signing key, media, or logs and every
container crash-loops on startup.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
nginx permits listen options on a single [::]:80 listener, and
puppet-nginx v8 rejects the '' workaround the PR used — a single-space
option string renders as none. Every Synapse worker parses the shared
homeserver.yaml and creates media_store_path at startup, so the media
bind mount is required on all synapse containers, not just main and
the media worker.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
profiles: :docker disables Docker's own iptables management; the
nftables: :rules::docker_ce parameters default to docker0/172.17.0.0/16,
while compose creates a hash-named bridge on 172.18.0.0/16 — so all
traffic to the containers (including nginx proxying to the published
127.0.0.1 ports) is dropped. Pin the network name and subnet in the
compose file and point the firewall parameters at them.
Signed-off-by: Michael Harp <mike@mikeharp.com>
Co-authored-by: Claude <noreply@anthropic.com>
Docker's embedded DNS resolves container names reliably; service-alias
registration proved flaky under docker 29.7/compose 5.4 with a custom
network (endpoints came up with Aliases: null and service names
returned NXDOMAIN). homeserver.yaml and the workers' instance_map dial
postgres/redis/synapse-* by exactly these names, so pinning
container_name makes resolution deterministic either way.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
docker_ce rules cover forwarded traffic; nginx and docker-proxy dial
the published 127.0.0.1 ports from the host, and that egress into the
container subnet hits the default-drop output chain — every proxied
request times out (nginx 504).
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
The official Synapse image ignores SYNAPSE_WORKER_LISTENERS-style
variables, so every worker ran without listeners (connection refused on
its instance_map port, nginx 502). Workers now get a second
--config-path with a Puppet-managed per-worker file; each worker's main
listener also carries the replication resource that instance_map
routing requires.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
Only synapse-main carried the build stanza; the other services
reference synapse-s3-local:latest image-only, so a clean host races
compose into pulling a nonexistent Hub image before (or instead of)
building — bring-up aborts nondeterministically. Declaring the same
build everywhere lets compose dedupe it and never pull.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
Build-time containers run on Docker's default bridge, which the
nftables docker_ce rules no longer cover once they are pointed at the
compose bridge — image builds cannot reach the network (pip fails
resolving pypi.org). Host networking for builds sidesteps the seam.

Pinning the base image matters because Synapse migrates the database
schema at startup: an innocent rebuild from :latest can silently jump
major versions with no rollback path. Renovate bumps pinned tags as a
matter of course.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
…ped Sensitive regex

- The delegation vhost served empty well-known files, breaking
  federation and client discovery for the whole server_name; they now
  carry the JSON documented in homeserver.yaml.epp, with
  application/json and CORS (client discovery is cross-origin).
- Media locations gain the Matrix 1.11+ authenticated paths
  (/_matrix/client/v1/media, /_matrix/federation/v1/media) routed to
  the media worker; the legacy path stays for compatibility.
- The sync location regex is replaced with the canonical worker-docs
  endpoints (the old one matched account-data/filters/OpenID instead),
  and its read timeout rises 60s -> 600s: sync long-poll timeouts are
  client-chosen and nginx was severing healthy polls.
- The Sensitive lookup_options regex said 'profile::' (singular), so it
  never matched and plain-string secrets failed the Sensitive[String]
  parameter types; fixed and deliberately scoped to profiles::matrix
  rather than fleet-wide.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
The base image's built-in HEALTHCHECK probes localhost:8008, which only
the main process serves — every worker reported unhealthy forever while
working fine. Workers now probe /health on their own listener port;
media-cron (a cron loop, nothing listens) disables the inherited check.
Signed-off-by: Michael Harp <mike@mikeharp.com>

Co-authored-by: Claude <noreply@anthropic.com>
Both registries are official per the Synapse install docs, but
Docker Hub rate-limits anonymous pulls per source IP, which bites
cloud hosts behind shared NAT, and element-hq is the canonical
org since the Element split.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Michael Harp <mike@mikeharp.com>
Compile-checks the profile with the role's composition on both
supported Ubuntu releases and pins the load-bearing details: the
delegation payloads, homeserver.yaml ownership for the container
UID, one config file per worker, and the egress nftables rule.
Eight of the fixes in this branch were compile errors that a spec
like this would have caught in CI.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Michael Harp <mike@mikeharp.com>
CI validates generated docs against the manifests; the matrix
profiles were never added to the reference.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Michael Harp <mike@mikeharp.com>
@miharp

miharp commented Aug 14, 2026

Copy link
Copy Markdown
Author

CI note: the one red job here is pre-existing. All 14 unit-test failures are profiles::grafana examples hitting a duplicate Package[certbot] that has failed every PR since March. Details and root cause in #203. Everything this branch adds is green, including the new profiles::matrix spec (13 examples, 0 failures).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants