The LiveKit JWT service checks each OpenID token over federation
against the user's homeserver. Server discovery found no delegation:
https://<server_name>/.well-known/matrix/server hit the redirect vhost
of web-opt-rdr-domains, which answered 301 to the dashboard and then
404, so the lookup fell back to <server_name>:8448. In compose OpenResty
runs in the host network and answers there; in swarm it publishes 80
and 443 only, the dial was refused and Element Call's token exchange
returned 401 M_UNAUTHORIZED (main run 40017166046, Nextcloud#12).
The role also wrote the delegation to `<well-known>/matrixserver`,
because the file path joined the directory and `server` without a
separator.
The file now lands at `<well-known>/matrix/server`, and the redirect
vhost serves `/.well-known/matrix/` from the shared well-known
directory, so discovery delegates to the Synapse domain on 443 in both
modes. Both flavors share tasks/utils/well_known.yml, which also
includes web-opt-rdr-domains for the server name when no play-wide
redirect claims it, so the delegation has a vhost there too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An earlier commit on this branch turned the three off because the ansible flavor ran
them without a published port, proxy entry, domain or certificate. They
now stay on and are wired end to end.
Element Call: MDAD's LiveKit server and JWT service get their own
entities in meta/services.yml with resource limits, local ports and a
public media port (TCP and UDP). The runner container publishes them
through container_ports, and the Synapse vhost proxies /livekit-server/
(websocket) and /livekit-jwt-service/ to them. The JWT service checks
each OpenID token against the user's homeserver over federation, and
federation was not reachable in this flavor: Synapse served it only on
a plain-HTTP listener that the runner published directly on 8448.
Synapse now serves the client and federation resources on one listener,
OpenResty serves the Synapse vhost on 443 and as default_server on the
federation port, the direct 8448 publish is gone, and the role writes
/.well-known/matrix/server as the compose flavor does. With a
self-signed CA the JWT container mounts the CA bundle as Synapse does.
Jitsi: a new services.jitsi entity follows the shared-service contract.
It is enabled in the ansible flavor off tor nodes. When web-app-jitsi is
in the inventory, Element points at that shared Jitsi and MDAD's own
Jitsi stays off. Otherwise MDAD runs its own Jitsi on jitsi.<domain>
with a front-proxy vhost, its web port published to the runner and its
videobridge on a public UDP port. The entity lists the jitsi domain, so
the web health probe skips it whenever Matrix does not serve it. The
jitsi_*_container_network overrides were dropped: MDAD reads none of
them, Jitsi always ran on its own network. Variant 0 covers the shared
case, variant 1 the internal one. On an onion-primary deploy both call
stacks stay off, so Element never advertises a clearnet call surface.
MDAD's Prosody keeps an account's first password, so after a credential
rotation jicofo failed SASL with the new one. The role now derives the
jicofo and videobridge passwords from the macaroon secret and recreates
both accounts on every deploy.
Heisenbridge stays on by default. It runs in the nested host network,
where MDAD's container-name URLs do not resolve, so its homeserver URL
and the appservice URL in its registration point at 127.0.0.1.
A bootstrap that a previous deploy left failed (for example after a
Docker Hub rate limit) is now reset and restarted, so a mode=update
deploy converges instead of failing on the stale unit state.
Playwright proves each path: the RTC spec fetches a LiveKit token from
the JWT service and opens the SFU signalling websocket, the Jitsi spec
checks Element's preferred domain and joins a conference (signing in
through Keycloak first when the shared Jitsi sits behind SSO), and the
Heisenbridge spec opens a DM with the bot and waits for its answer to
"help".
Closes#1165
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ansible flavor's runner starts its own docker daemon, and that daemon
pulled every MDAD image straight from Docker Hub and ghcr.io, past the
registry cache the stack's own daemon uses. MDAD pulls with
`pull: always`, so every bootstrap spent the shared anonymous Docker Hub
quota; locally a single run exhausted it and died with
"toomanyrequests" even for images already on disk.
The role now reads the proxy of the stack's docker.service. When there
is one, it mounts a docker.service drop-in into the runner with the same
HTTP_PROXY, HTTPS_PROXY and NO_PROXY, and pins the proxy host through
container_extra_hosts. When the stack's daemon trusts the registry
cache's MITM CA, the drop-in fetches that CA from the proxy and runs
update-ca-certificates before the inner daemon starts. Without a proxy
on the stack's daemon nothing changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From triage of run 33923190220. Each fix is independently complete.
penpot: ansible templates delegate_to before the task's merged when, and AnsibleUndefinedVariable escapes get_delegated_vars_and_hostname. PENPOT_BACKEND_HOST_NODE is set only by the resolve include, so the bootstrap task hard-failed on every host that include skipped instead of skipping with it. A block-level when cannot prevent this; move the tasks into a dynamic include.
matrix: the MDAD reconfigure looked for volume matrix_matrix_mdad_state (real name matrix_mdad_state) and container matrix-mdad (real name matrix). Both if-bodies were skipped while the task still reported changed, so a rotated S3 secret never reached synapse and every media write got SignatureDoesNotMatch. Move the shell into its own script and pass the real names through the environment. Also add no_log to the vars.yml render, whose diff printed 16 secrets in cleartext.
n8n: when the settings file and N8N_ENCRYPTION_KEY disagree, oclif's findCommand throws for every command, so 'n8n export:credentials' is unreachable in exactly the state that would need it. The re-key is one-way; pin it rotatable: false.
swarm-drill: distros.sh sees only rc=124 and cannot tell a wedged deploy from one the sweep deadline cut short. Say so where the governor caps it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.
Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.
Freed from the binding, the names take the emoji prefix every other task line
already has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meta/volumes.yml says where a volume lands inside its container. Wherever a
second file spelled the same path out, the two drifted independently: the mount
moves, the consumer keeps pointing at the old place, and nothing fails until the
process looks for content that is not there. e031b7f61 fixed exactly this for
web-app-moodle; this generalises it and makes the rule hold.
The lookup gains what a caller needs. `targets` maps each mounting service to
its container path, and `target` is the one path an entry mounts at -- present
only when every mount agrees. An entry that lands at different paths per service
omits `target` rather than picking one, and the caller names the service:
"{{ lookup('volume', application_id, 'prometheus_config').target }}"
"{{ lookup('volume', 'web-app-prometheus', 'host_sys').targets['node-exporter'] }}"
66 literals across 48 role files now derive. Replacement only happened where the
path stood alone -- /srv/app inside /srv/app/data is a different path -- and
every rewritten file was re-parsed, Jinja and YAML alike. Three sites the tool
refused rather than guess were done by hand: zammad's path sat in a Jinja list
literal, seaweedfs' inside a quoted string feeding a filter, and openresty's
/var/run/docker.sock is not a mount target at all but the socket on the host,
which only happens to spell the same string. That one carries the single
nocheck.
tests/lint/repository/spot/test_volumes_path_single_spot.py keeps it that way.
roles/*/files/ is out of scope by construction: it is copied verbatim into an
image and executed there, so a Dockerfile or entrypoint has no Ansible to
evaluate a lookup with.
One defect this found, and it was in the tooling rather than the roles: deciding
ambiguity AFTER filtering short paths made web-app-prometheus' host_sys look
single-valued -- it mounts /sys for cadvisor and /host/sys for node-exporter,
and /sys is below the length filter. The template would have rendered an
undefined `.target`. Both the converter and the rule now decide ambiguity over
every target and filter afterwards, and that call reads targets['node-exporter'].
Verified by resolving rather than by reading: all 70 accesses in the diff were
run against the real registry and every one yields a non-empty path equal to the
literal it replaced. make test-lint: 328 passed.
Not verified: no Ansible render, so a call site whose surrounding context cannot
build the applications registry would still fail at deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The staging task listed three files by hand while the runner Dockerfile
COPYs five. 0a581f662 added hostpin.sh and hostpin.service together with
their two COPY lines and did not extend the list, so the build failed
deterministically:
#18 ERROR: failed to calculate checksum of ref ...: "/hostpin.sh": not found
failed to solve: failed to compute cache key
and compose pull retried it ten times -- a missing file no retry can heal.
Both files and the defect are branch-local: neither exists on origin/main.
Copy the directory instead of enumerating it. The per-file modes the list
carried were fiction anyway: every source is 100644 in git, including
bootstrap.sh which the list published as 0755, and the Dockerfile chmods
0755 on both scripts itself, so the staged mode never reached the image.
The point is not the two missing entries. A list that must be updated
whenever a file is added is silent exactly when it is forgotten -- here it
went wrong inside a single commit that touched the Dockerfile and the
directory at the same time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ActiveState=failed` only says the oneshot exited non-zero, and the unit runs
two phases: `ansible-galaxy install -r requirements.yml --force` and then
`ansible-playbook setup.yml` (bootstrap.sh:39-44, under set -euo pipefail).
Claiming the playbook failed sends the reader looking for an inner PLAY RECAP.
In run 30781723102 the web-app-nextcloud 3 job died in the first phase, at role
21 of 31, on a git clone that got HTTP 503. The container journal logs
"starting ansible-galaxy install" and never reaches "galaxy install done" or
"starting ansible-playbook setup.yml", so there was no recap to find.
Wording only; the branch, its condition and the diagnostics are unchanged. The
cause of that 503 is still open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Since the inner mdad dockerd actually starts (storage-driver fix), its
default docker0 claims 172.17.0.1 inside the container netns and
shadows exactly the resolver address the swarm inventory hands to every
container via networks.internet.dns, so the bootstrap's ansible-galaxy
died on 'Could not resolve host: github.com' one second after boot.
Declare the nested-docker bridge ranges as a network SPOT
(NETWORK_NESTED_DOCKER_BIP / _ADDRESS_POOLS, deliberately outside
172.16.0.0/12 and away from the 192.168.244.0/24 lab net) and render
the runner's daemon.json from it at staging time instead of shipping a
static file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mdad runner pinned storage-driver overlay2, but on swarm test nodes
the container's /var/lib/docker sits on overlayfs and the kernel rejects
overlay-on-overlay upperdirs, so the inner dockerd exited, the bootstrap
oneshot failed by dependency and Synapse never came up (proven by the
rescue-diagnostics journal of run 29356373746). Drop the pin so dockerd
auto-detects and ship fuse-overlayfs as the nested fallback; real hosts
keep overlay2.
Replace the 30min blind HTTP poll with a bootstrap terminal-state wait:
fail fast with diagnostics when the inner docker daemon or the bootstrap
playbook fails, probe Synapse only after the oneshot completed, and run
the journal dump on every failure path instead of behind MODE_DEBUG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- matrix 02_render_inventory: the mdad-state volume does not exist yet on a
fresh install, so `container volume inspect` errored the reconfigure task
(`no such volume`) on the row-1 baseline. Guard the marker drop behind the
inspect succeeding; the runner bounce still runs if the container is up.
- test_coverage: read services.yml via utils.cache.files.read_text so the
no-uncached-file-reads lint passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _shared.js: wrap the boot-poll delay in resolveTimeout (raw-timeout lint)
- 02_render_inventory: docker -> container wrapper (engine-abstraction lint)
- seaweedfs: api becomes the primary canonical so the deploy readiness
probe hits a served vhost when the frontend (filer/master) is disabled
- seaweedfs frontend: exempt from variant pinning — the onion gate is a
hard invariant, no variant may force the frontend on a tor node
- test_coverage: add the `# nocheck: variants-coverage` exempt (same
mechanism as bond-baseline) for such invariant flags
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Synapse on an onion node could never complete SSO: it has no Tor, so the
http://auth.<onion> issuer was unresolvable server-side (discovery/jwks/
token), and Synapse hard-rejects non-https issuers on top.
- runner/hostpin.{sh,service}: pin the onion auth host to the runner's real
default gateway (node OpenResty) from /proc/net/route, Before=docker.service
so nested containers see it from their first request; host-gateway is
unusable in nested DinD (resolves to the inner docker0)
- Synapse bind-mounts the runner's /etc/hosts; token iss stays the onion
issuer the browser uses
- bootstrap.service: PassEnvironment (systemd units do not inherit container
env; MATRIX_MDAD_PLAYBOOK_TAGS only worked via its fallback default)
- vars.yml.j2: skip_verification on onion (Synapse's escape hatch for
issuers without https; Tor provides transport security per RFC 7686)
- 02_render_inventory: drop the MDAD bootstrap marker + bounce the runner on
inventory changes — mode=update reconfigures the nested stack at all now
- MATRIX_SERVER_NAME follows matrix_domain onto the onion so MXIDs built from
it (DM spec, bridge configs) reference users that exist
- specs: shared Firefox onion fixture (16 files), gotoElementApp reload-retry
against Element's Tor-triggered init race, seaweedfs spec gates on the
filer frontend, env uses canonical_url consumer=
Verified in-container: SSO 302 to onion Keycloak, admin+biber OIDC login,
E2EE DM admin<->biber, CSP — all green over Tor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm mode the container_address lookup returns a $(/usr/bin/resolve-container-id ...) fragment that must expand at exec time; piping it through Jinja quote hands docker the literal string and every exec fails with 'No such container' (broke all three snipe-it CI variants). Remove the quote at the seven snipe-it sites and the live matrix/openproject sites, convert openproject's argv exec to shell so the fragment can expand, split the matrix bridge restart per deployment mode with a manager gate, and mark the compose-only or eval-contract sites with nocheck. A new lint (container-address-quote) bans the pattern repo-wide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DiD runner ran as a separate compose service matrix-mdad while the app entity is matrix, so the swarm overlay upstream needed a service_key override to reach matrix_matrix-mdad. Name the service, container_name and entity all matrix (MATRIX_MDAD_SERVICE, MATRIX_MDAD_CONTAINER, services.matrix.name) so the overlay DNS is matrix and resolve_upstream returns matrix:8008 by default. Drop the webserver_proxy_service_key override; keep proxy_internal_port (synapse 8008, element 8080).
Also drop the now-dead matrix-mdad meta entry (resource_filter resolves to the entity, so its cpus/mem were never read), flavor-gate the volume mounts (mdad to ansible, synapse/element to compose) so the ansible flavor no longer declares compose-only volumes/configs, and have diagnostics.sh read the container name from the MATRIX_CONTAINER env instead of hardcoding it. Renderer + unit validated: swarm matrix:8008/8080, compose 127.0.0.1:8021/8022.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.
- emit emoji names at the source: cli/build/include generator now writes
"🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ansible-flavor stages the matrix-mdad runner build context into MATRIX_MDAD_RUNNER_BUILD_DIR via stack_host_copy, but nothing created that directory first, so the copy failed 'Destination directory ... does not exist'. Ensure the dir (delegate_to STACK_HOST, run_once) before the copy loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The matrix-mdad runner image's compose build.context pointed at the Ansible controller's role tree ({{ application_id | abs_role_path_by_application_id }}/files/flavor/ansible/runner). On swarm the pre-deploy 'compose build' handler runs on the manager (delegate_to STACK_HOST run_once), where roles/web-app-matrix/files/ does not exist, so buildx failed: 'unable to prepare context: path ... not found'. Stage the four runner build-context files onto STACK_HOST via stack_host_copy and point the build context at that host-local instance dir, mirroring svc-db-openldap. sys-stk-backend's flush is deferred so the build runs after the stage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconcile main's object-store feature and pre-swarm deploy tasks with the branch's compose/networks/volumes SPOT and swarm lints.
objstore (seaweedfs/minio): standalone compose + embedded service templates migrated to the container_image/container_volumes/container_networks/compose_volumes/compose_networks/depends_on lookups; meta/volumes.yml on the canonical dict schema with s3.json and nginx.conf as type:config; embedded services made swarm-compatible via compose_only/compose_restart.
deploy tasks (erpnext, opentalk, seaweedfs, shopware, snipe-it, wordpress, yourls): container exec routed through the container_address resolver and gated on IS_STACK_HOST, shell module plus stack_host_template, no chdir or compose-verb forms.
Also removed orphaned vars, restored MAILU_INSTANCE_DIR, freed the seaweedfs subnet (192.168.206.0/24), stripped literal quotes from the nextcloud/bookwyrm env templates, and routed main's objstore compose_volumes tests through dir_var_lib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both flavors integrate with Infinito's central postgres (no more MDAD
bundled DB inside the DiD container). The connection vars live in a
single set of MATRIX_POSTGRES_* constants in vars/main.yml and are
referenced by both flavors' templates so the source-of-truth is
explicit and identical.
- vars/main.yml: MATRIX_MDAD_POSTGRES_* renamed to MATRIX_POSTGRES_*
(drop MDAD prefix — these are flavor-agnostic) + add
MATRIX_POSTGRES_DATABASE.
- templates/flavor/ansible/vars.yml.j2 (was mdad-vars.yml.j2):
postgres_enabled: false; wire postgres_connection_*/synapse
database_* to MATRIX_POSTGRES_* + matrix_synapse_container_network:
host so the inner Synapse inherits matrix-mdad's outer networks and
resolves `database` / `openldap` by hostname. valkey_enabled: false
(Synapse single-process; central redis path not used here).
- templates/flavor/ansible/hosts.j2 (was mdad-hosts.j2): renamed, no
content change. mdad- prefix dropped since the file is already
under flavor/ansible/.
- templates/flavor/ansible/services.yml.j2: matrix-mdad joins the
central `postgres` network (conditional on DB shared) and
`openldap` network (conditional on MATRIX_LDAP_ENABLED).
- templates/flavor/compose/synapse/homeserver.yaml.j2 and the six
templates/flavor/compose/mautrix/*.config.yml.j2 templates: switch
inline `lookup('database', application_id, 'host'/'username'/...)`
calls to MATRIX_POSTGRES_* (same constants the ansible flavor uses).
- tasks/flavor/ansible/: renumber 01-05 cleanly
- 03_database.yml (was 02a_database.yml) — sys-svc-rdbms include
- 04_run_playbook.yml (was 03_run_playbook.yml)
- 05_proxy_wiring.yml (was 04_proxy_wiring.yml)
- tasks/flavor/ansible/main.yml + 02_render_inventory.yml + README.md:
update references to the renamed files.
- tasks/flavor/compose/02_create-and-seed-database.yml: switch host/port
lookups to MATRIX_POSTGRES_* too.
Tests: 186/186 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Failing CI run 27010233874 (job 79718652049) and the local DiD test
both timed out at `probe Synapse client API on host loopback` with
`Connection reset by peer` for the full 140s retry window. Workflow
rootcause inspection of the matrix-mdad container surface identified
two compounding issues:
1. `matrix_mdad_systemd:/etc/systemd/system` named volume shadowed the
image-baked `multi-user.target.wants/matrix-mdad-bootstrap.service`
enable symlink. First-boot's empty named volume masks the image
content so systemd never sees the unit as enabled — bootstrap never
runs, MDAD never installs, Synapse never starts. Smoking gun.
2. 60×2s = 140s probe window was an order of magnitude too short for a
cold MDAD bootstrap (galaxy install + image pulls + ansible-playbook
setup-all on a fresh container). Even with the unit enabled the
probe would race the bootstrap.
Changes:
- services.yml.j2: drop `matrix_mdad_systemd:/etc/systemd/system`
mount; MDAD-written units live in the container's writable layer
alongside the image's enable symlinks. Add `matrix_mdad_state:
/var/lib/matrix-mdad` so the bootstrap marker survives container
restarts. Normalize federation port publish to `DOCKER_BIND_HOST`.
- compose.yml.j2: rename declared volume `matrix_mdad_systemd` ->
`matrix_mdad_state`. Inside the `matrix_flavor == 'ansible'` branch
only — compose flavor untouched.
- bootstrap.service: drop `network-online.target` (never fires in a
container without systemd-networkd-wait-online). Add
`ExecStartPre=/usr/bin/docker info` for fail-fast if the inner
dockerd never came up.
- bootstrap.sh: log markers + per-step timing so the systemd journal
inside matrix-mdad shows a clear timeline.
- 04_proxy_wiring.yml: 60×2s -> 180×5s = 900s; on probe failure under
MODE_DEBUG, run a new diagnostics.sh script (container logs +
systemctl --failed + journalctl + docker.service status) so the next
iteration has actionable output instead of a silent retry stack.
make test: 186/186 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- bootstrap.sh / bootstrap.service: systemd doesn't inherit Dockerfile
ENV PATH, so the oneshot couldn't find ansible-galaxy. Export PATH in
the script and set Environment=PATH in the unit explicitly.
- 04_proxy_wiring.yml: rewrite the inner-docker admin-register exec as
shell scalar with `# nocheck: raw-docker` (the inner docker CLI talks
to matrix-mdad's DiD daemon; not host docker).
- vars/main.yml: drop now-unused MATRIX_MDAD_POSTGRES_* + MATRIX_REDIS_*
vars (DiD uses bundled postgres + valkey).
make test: 186/186 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
matrix-mdad is now a long-running compose service with systemd PID 1
and its own bundled docker daemon. MDAD writes its systemd unit files
and runs matrix-synapse/coturn/element/bridges/jitsi/element-call
entirely INSIDE that container via the inner docker daemon. Nothing
leaks to the host beyond standard /opt/compose/matrix/ role state.
Container layout:
- /sbin/init (systemd) as PID 1
- bundled docker.io daemon (inner DinD)
- bootstrap.service oneshot: runs `ansible-playbook setup.yml` once on
first boot; marker at /var/lib/matrix-mdad/bootstrap.done
- Named volumes: matrix_mdad_docker (inner /var/lib/docker),
matrix_mdad_systemd (/etc/systemd/system), matrix_mdad_matrix (/matrix)
- /sys/fs/cgroup:rw + cgroup: host for systemd-in-container
- Outer compose publishes synapse/element/federation ports to host loopback
mdad-vars.yml.j2:
- postgres_enabled: true (MDAD bundled postgres inside DiD)
- valkey_enabled: true (bundled redis)
- exim_relay_enabled: true when no outbound Mailu
- host_bind_port: 0.0.0.0 inside container so outer compose port-publish
can forward to host loopback
Dropped (no longer needed):
- 02a_database.yml — MDAD provisions postgres inside the DiD container
- 00_cleanup_mdad_residue.yml — no host residue to clean
- ensure rshared mount task — no host bind-propagation needed
- container wait matrix-mdad-runner — service is long-running not one-shot
- matrix_synapse_container_additional_networks_custom — DiD has its own
internal docker network
- matrix_synapse_container_extra_arguments CA mount — bundled CA inside
- coturn_container_additional_volumes — anonymous volumes live in inner
docker daemon, outer hlth-volumes can't see them
04_proxy_wiring.yml:
- admin user creation now uses two-hop exec: outer `container exec
matrix-mdad` -> inner `docker exec matrix-synapse register_new_matrix_user`
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>