Files
matrix-docker-traefik/README.md
snoopy 708312c80c Initial commit: Matrix stack (Synapse + Synapse-Admin + Element Web) behind Traefik
Fully dockerized deployment with automatic Let's Encrypt certificates via
Traefik's static file provider (no docker.sock needed), bind-mounted data
folders instead of named volumes, and domain/secrets kept out of git via
.env + rendered templates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 16:40:59 +02:00

5.1 KiB

Matrix stack (Synapse + Synapse-Admin + Element Web) behind Traefik

A self-hosted Matrix homeserver, its admin panel, and a web client, all running in Docker, fronted by Traefik with automatic Let's Encrypt certificates. No container publishes a port to the host except Traefik (80/443). All persistent data lives in plain host folders under this project (no named Docker volumes), and the real domain name and secrets live only in a local, git-ignored .env file.

Architecture

Internet ──80/443──> Traefik ──(docker network, no published ports)──┬──> Synapse (matrix.DOMAIN)
                                                                       ├──> Element Web (element.DOMAIN)
                                                                       ├──> Synapse-Admin (admin.DOMAIN)
                                                                       ├──> nginx: /.well-known/matrix/client (DOMAIN)
                                                                       └──> Traefik dashboard (traefik.DOMAIN, basic auth)
                                                              Synapse ──> PostgreSQL (internal network only)

Matrix server_name is the bare domain (DOMAIN), so user IDs look like @alice:DOMAIN. The homeserver's actual HTTP API is served from matrix.DOMAIN, discovered by clients via .well-known/matrix/client delegation served from the bare domain. Federation is intentionally disabled (private/closed server) — see synapse/data/homeserver.yaml.

Traefik uses its static file provider for routing (not the Docker provider / docker.sock), so Traefik never needs access to the Docker socket — a smaller attack surface, and it sidesteps Docker-API-version compatibility issues some environments have with Traefik's Docker provider.

Prerequisites

  • Docker Engine + Docker Compose v2
  • A domain name with DNS A/AAAA records pointing at this server for:
    • DOMAIN (e.g. example.com)
    • matrix.DOMAIN
    • element.DOMAIN
    • admin.DOMAIN
    • traefik.DOMAIN (subdomain names are configurable in .env)

Setup

  1. Copy the environment template and fill in your real domain/secrets:

    cp .env.example .env
    

    Edit .env:

    • DOMAIN and the four *_SUBDOMAIN values
    • ACME_EMAIL — a real address you control (used by Let's Encrypt)
    • TRAEFIK_DASHBOARD_AUTH — generate with openssl passwd -apr1
    • POSTGRES_PASSWORD — generate with openssl rand -hex 24
    • REGISTRATION_SHARED_SECRET — generate with openssl rand -hex 32 (this one gets baked into homeserver.yaml in step 3 below; keep both in sync if you change it later)
  2. Render the templated config files (Element, Synapse-Admin, the well-known JSON, and Traefik's static routes) with your real domain:

    ./scripts/render-templates.sh
    

    Re-run this any time you change domain-related values in .env.

  3. Generate the initial Synapse config (first time only):

    docker run --rm \
      -e SYNAPSE_SERVER_NAME=<your DOMAIN> \
      -e SYNAPSE_REPORT_STATS=no \
      -v "$(pwd)/synapse/data:/data" \
      matrixdotorg/synapse:latest generate
    

    Then edit synapse/data/homeserver.yaml:

    • Remove federation from the listener's resources (already disabled by federation_domain_whitelist: [], this drops the endpoint too)
    • Replace the database: block with a psycopg2 block pointing at the postgres service, using the credentials from .env
    • Add public_baseurl: "https://matrix.<DOMAIN>/"
    • Add federation_domain_whitelist: [] and enable_registration: false

    (This repo's own deployment already has this file populated under synapse/data/ locally — it's git-ignored, so a fresh clone always starts from scratch here.)

  4. Fix data directory ownership so the containers can write to their bind-mounted folders:

    chown -R 70:70 postgres/data      # postgres container's uid/gid
    touch traefik/acme/acme.json && chmod 600 traefik/acme/acme.json
    
  5. Start everything:

    docker compose up -d
    

    Watch docker compose logs -f traefik until certificates are issued.

  6. Create your first (admin) user:

    docker compose exec synapse register_new_matrix_user \
      -c /data/homeserver.yaml http://localhost:8008 -a
    

Notes

  • Federation is off. This is a private server for your own users only. To enable it, remove federation_domain_whitelist: [] (or populate it), add federation back to the listener's resources, and expose the federation port through a Traefik router (typically still 443 via .well-known/matrix/server delegation, or a dedicated 8448 entrypoint).
  • Registration is closed. New accounts are created via register_new_matrix_user (or through Synapse-Admin once logged in as an admin), not via public sign-up in Element.
  • Traefik dashboard is reachable at https://traefik.DOMAIN, behind HTTP basic auth (TRAEFIK_DASHBOARD_AUTH in .env).
  • All data directories (postgres/data, synapse/data) are plain host folders, not Docker named volumes — back them up like any other files.