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>
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.DOMAINelement.DOMAINadmin.DOMAINtraefik.DOMAIN(subdomain names are configurable in.env)
Setup
-
Copy the environment template and fill in your real domain/secrets:
cp .env.example .envEdit
.env:DOMAINand the four*_SUBDOMAINvaluesACME_EMAIL— a real address you control (used by Let's Encrypt)TRAEFIK_DASHBOARD_AUTH— generate withopenssl passwd -apr1POSTGRES_PASSWORD— generate withopenssl rand -hex 24REGISTRATION_SHARED_SECRET— generate withopenssl rand -hex 32(this one gets baked intohomeserver.yamlin step 3 below; keep both in sync if you change it later)
-
Render the templated config files (Element, Synapse-Admin, the well-known JSON, and Traefik's static routes) with your real domain:
./scripts/render-templates.shRe-run this any time you change domain-related values in
.env. -
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 generateThen edit
synapse/data/homeserver.yaml:- Remove
federationfrom the listener'sresources(already disabled byfederation_domain_whitelist: [], this drops the endpoint too) - Replace the
database:block with apsycopg2block pointing at thepostgresservice, using the credentials from.env - Add
public_baseurl: "https://matrix.<DOMAIN>/" - Add
federation_domain_whitelist: []andenable_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.) - Remove
-
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 -
Start everything:
docker compose up -dWatch
docker compose logs -f traefikuntil certificates are issued. -
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), addfederationback to the listener'sresources, and expose the federation port through a Traefik router (typically still 443 via.well-known/matrix/serverdelegation, or a dedicated8448entrypoint). - 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_AUTHin.env). - All data directories (
postgres/data,synapse/data) are plain host folders, not Docker named volumes — back them up like any other files.