Both apps demanded a domain and Traefik. That was over-constrained: LibrePortal ships WireGuard, Headscale and private ports, so LAN and VPN-only is a first-class deployment here, and Rocket.Chat and Mattermost already prove chat apps work fine on http://<lan-ip>:<port>. The gate on Matrix rested on a mistake of mine: server_name being permanent. server_name and public_baseurl are independent — the identity can be a domain you own with no DNS behind it while clients reach the server on a LAN address, so federation can be switched on later by adding DNS and TLS, with no rebuild and no lost history. CFG_MATRIX_SERVER_NAME now exposes exactly that, and the install warns when it falls back to the machine's IP. What is genuinely lost without a domain is stated where it belongs, at install: Matrix cannot federate and Element's mobile apps want HTTPS; Stoat cannot do camera or microphone, because browsers gate getUserMedia on a secure context and a VPN does not change that, the check being on the URL scheme. Both now derive their URL from the port that was actually allocated. Since ports are only assigned during compose-up, each writes a best guess before start and corrects it afterwards, restarting only when the value really changed. Three bugs found while proving it works end to end: - The Synapse image writes /data as its UID/GID env, default 991, which under rootless is a host sub-UID owning nothing — so the generated signing key could not be moved by the install user. Both the generate container and the service now run as the same identity USER_TAG resolves to. - Element's config.json is bind-mounted as a file, and docker silently creates a DIRECTORY when the source is missing. An early return left exactly that landmine, which then broke every later run. It is written first now, and a stale directory is cleared. - A successful admin registration was reported as an error: checkSuccess read $? after an intervening [[ ]] test rather than the command's own status. Verified with no domain and no Traefik installed: Synapse answers /_matrix/client/versions and /health on http://<ip>:<port>, admin login returns a token, and Element is configured against the corrected base_url. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
7.2 KiB
YAML
126 lines
7.2 KiB
YAML
networks:
|
|
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
external: true
|
|
|
|
services:
|
|
# Synapse — the homeserver. Everything that matters lives in
|
|
# ./data/homeserver.yaml, written by matrix_install_post_compose from
|
|
# resources/homeserver.yaml; Synapse takes no meaningful configuration from
|
|
# the environment, so there is little to see here.
|
|
matrix-synapse: #LIBREPORTAL|SERVICE_TAG_1|matrix-synapse
|
|
container_name: matrix-synapse
|
|
image: matrixdotorg/synapse:v1.158.0 #LIBREPORTAL|MATRIX_VERSION_TAG|v1.158.0
|
|
# Synapse writes the media store, and under rootless Docker the image's
|
|
# own uid maps to a host sub-UID that owns nothing. Same fix as the
|
|
# other apps: run as whoever owns the bind mounts.
|
|
user: "USER_DATA" #LIBREPORTAL|USER_TAG|USER_DATA
|
|
restart: unless-stopped
|
|
# GLUETUN_OFF_BEGIN
|
|
ports:
|
|
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
|
|
# GLUETUN_OFF_END
|
|
volumes:
|
|
- ./data:/data
|
|
environment:
|
|
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
|
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
|
|
# These are not redundant with `user:` above. The image's entrypoint
|
|
# checks whether it is root and, if so, su-execs down to UID:GID —
|
|
# which default to 991:991. So a container started as 0:0 still ends
|
|
# up running as 991, and 991 maps to a host sub-UID that does not own
|
|
# the bind mount: Synapse then dies reading its own chmod-600
|
|
# homeserver.yaml. Pinning them to the same identity as USER_TAG
|
|
# stops the entrypoint dropping to a user that cannot read anything.
|
|
- UID=MATRIX_RUN_UID_DATA #LIBREPORTAL|MATRIX_RUN_UID_TAG|MATRIX_RUN_UID_DATA
|
|
- GID=MATRIX_RUN_GID_DATA #LIBREPORTAL|MATRIX_RUN_GID_TAG|MATRIX_RUN_GID_DATA
|
|
depends_on:
|
|
- matrix-postgres
|
|
labels:
|
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
|
libreportal.backup.db: "postgres:matrix-postgres:postgres:"
|
|
# The media store holds every uploaded file and avatar. It is not in
|
|
# the database, so without this it would not come back on restore.
|
|
libreportal.backup.files: "matrix-synapse:/data/media_store:data/media_store"
|
|
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
|
# TRAEFIK_PORT_1_BEGIN
|
|
traefik.http.routers.matrix-synapse.entrypoints: web,websecure
|
|
traefik.http.routers.matrix-synapse.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
|
|
traefik.http.routers.matrix-synapse.tls: true
|
|
traefik.http.routers.matrix-synapse.tls.certresolver: production
|
|
traefik.http.services.matrix-synapse.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
|
|
traefik.http.routers.matrix-synapse.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
|
|
# TRAEFIK_PORT_1_END
|
|
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
healthcheck:
|
|
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
|
|
# GLUETUN_OFF_BEGIN
|
|
networks:
|
|
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
|
|
# GLUETUN_OFF_END
|
|
# GLUETUN_ON_BEGIN
|
|
# network_mode: "container:gluetun-service"
|
|
# GLUETUN_ON_END
|
|
|
|
# Element web — a static single-page app served by nginx. It talks to
|
|
# Synapse from the user's browser, not server-side, so it needs no link to
|
|
# the homeserver container beyond the base_url baked into config.json.
|
|
matrix-element: #LIBREPORTAL|SERVICE_TAG_2|matrix-element
|
|
container_name: matrix-element
|
|
image: vectorim/element-web:v1.12.25 #LIBREPORTAL|MATRIX_ELEMENT_VERSION_TAG|v1.12.25
|
|
restart: unless-stopped
|
|
# GLUETUN_OFF_BEGIN
|
|
ports:
|
|
- "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2
|
|
# GLUETUN_OFF_END
|
|
volumes:
|
|
- ./element/config.json:/app/config.json:ro
|
|
environment:
|
|
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
|
labels:
|
|
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
|
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
|
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
|
# TRAEFIK_PORT_2_BEGIN
|
|
traefik.http.routers.matrix-element.entrypoints: web,websecure
|
|
traefik.http.routers.matrix-element.rule: Host(`DOMAINSUBNAME_DATA_2`) #LIBREPORTAL|DOMAINSUBNAME_TAG_2|DOMAINSUBNAME_DATA_2
|
|
traefik.http.routers.matrix-element.tls: true
|
|
traefik.http.routers.matrix-element.tls.certresolver: production
|
|
traefik.http.services.matrix-element.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2
|
|
traefik.http.routers.matrix-element.middlewares: MIDDLEWARE_DATA_2 #LIBREPORTAL|MIDDLEWARE_TAG_2|MIDDLEWARE_DATA_2
|
|
# TRAEFIK_PORT_2_END
|
|
traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
healthcheck:
|
|
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
|
|
# GLUETUN_OFF_BEGIN
|
|
networks:
|
|
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2
|
|
# GLUETUN_OFF_END
|
|
# GLUETUN_ON_BEGIN
|
|
# network_mode: "container:gluetun-service"
|
|
# GLUETUN_ON_END
|
|
|
|
# No `user:` override — the postgres entrypoint starts as root, chowns
|
|
# PGDATA and drops privileges, which works under rootless because
|
|
# container-root is the install user that owns the mount.
|
|
matrix-postgres: #LIBREPORTAL|SERVICE_TAG_3|matrix-postgres
|
|
image: postgres:15-alpine
|
|
container_name: matrix-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
- TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA
|
|
- POSTGRES_USER=synapse
|
|
- POSTGRES_PASSWORD=MATRIX_DB_PASSWORD_1_DATA #LIBREPORTAL|MATRIX_DB_PASSWORD_1_TAG|MATRIX_DB_PASSWORD_1_DATA
|
|
- POSTGRES_DB=synapse
|
|
# Not optional. Synapse refuses to start against a database with any
|
|
# other collation or ctype — it needs deterministic byte ordering for
|
|
# its indexes, and a C.UTF-8 locale is the only thing that gives it.
|
|
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
|
|
volumes:
|
|
- ./postgres:/var/lib/postgresql/data
|
|
networks:
|
|
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
|
ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3
|