281 lines
11 KiB
Markdown
281 lines
11 KiB
Markdown
|
|
+++
|
||
|
|
title = 'Hosting [matrix] Synapse with Docker Compose'
|
||
|
|
description = 'Including configuration and initialization into docker compose files to make setup faster.'
|
||
|
|
date = 2025-12-08T14:10:00-05:00
|
||
|
|
draft = false
|
||
|
|
categories = ['guides']
|
||
|
|
tags = ['Matrix', 'Synapse', 'Element', 'Docker', 'Compose', 'Portainer', 'HAProxy']
|
||
|
|
disableToc = false
|
||
|
|
showFeature = false
|
||
|
|
+++
|
||
|
|
|
||
|
|
# A workaround for docker container intialization requirements when using docker compose.
|
||
|
|
<!--more-->
|
||
|
|
|
||
|
|
<div style="width:100%; max-width:640px; aspect-ratio:16/9; position:relative; overflow:hidden;">
|
||
|
|
<iframe src="/matrix-synapse-docker-compose/matrix-synapse-thumbnail.html" style="border:none; width:200%; height:200%; transform:scale(0.5); transform-origin:0 0;"></iframe>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
Synapse is a great implementation of the [matrix] federated chat platform, but the [official documentation](https://matrix-org.github.io/synapse/latest/setup/installation.html) expects you to run several manual initialization steps before your server is ready to use. This gets tedious when you're just want to use a docker compose file to plug in your info and get going, especially if you are using tools like Portainer to administrate your containers.
|
||
|
|
|
||
|
|
This guide shows you how to bundle the entire initialization process into your docker-compose.yml so your [matrix] server goes from zero to fully operational with a single `docker compose up`. This compose file also includes element, which is a web-based client frontend for the [matrix] server. Note that I use HAProxy to terminate SSL within my network, so you may want to use traefic / let's encrypt or some other proxy between your matrix server and the web if you are not in the same boat. Even if you don't use HAProxy I think this can be a useful guide!
|
||
|
|
|
||
|
|
## The Problem
|
||
|
|
|
||
|
|
The standard [matrix] Synapse setup requires you to:
|
||
|
|
|
||
|
|
1. Generate the initial homeserver configuration
|
||
|
|
2. Create a registration shared secret
|
||
|
|
3. Wait for the server to start
|
||
|
|
4. Manually run register_new_matrix_user to create your admin account
|
||
|
|
5. Configure email settings
|
||
|
|
6. Set up your public base URL
|
||
|
|
|
||
|
|
If you're deploying through a UI like Portainer or want to version control your entire setup, having to SSH into your server and run commands defeats the purpose. We can do better.
|
||
|
|
|
||
|
|
## The Solution
|
||
|
|
|
||
|
|
We're going to use a custom entrypoint script that handles all the initialization automatically. All of our personal information will be stored in environment variables for easy access. These can be directly in the compose file, they can be in a separate .env file, or configured within the Portainer GUI. The script will check if this is the first run, generate the configuration if needed, inject our custom settings, start the server, and create the admin user once Synapse is ready.
|
||
|
|
|
||
|
|
Here's a minimally censored docker-compose configuration to make it easier to understand:
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
version: '3'
|
||
|
|
|
||
|
|
services:
|
||
|
|
synapse:
|
||
|
|
image: docker.io/matrixdotorg/synapse:latest
|
||
|
|
restart: unless-stopped
|
||
|
|
environment:
|
||
|
|
SYNAPSE_SERVER_NAME: matrix.karsttech.com
|
||
|
|
SYNAPSE_REPORT_STATS: no
|
||
|
|
SYNAPSE_ADMIN_USER: admin
|
||
|
|
SYNAPSE_ADMIN_PASSWORD: [ADMIN_PASSWORD]
|
||
|
|
TZ: America/New_York
|
||
|
|
# SMTP Configuration
|
||
|
|
SMTP_HOST: smtp.protonmail.ch
|
||
|
|
SMTP_PORT: 587
|
||
|
|
SMTP_USER: automation@karsttech.com
|
||
|
|
SMTP_PASS: [SMTP_PASSWORD]
|
||
|
|
SMTP_FROM: "Matrix <automation@karsttech.com>"
|
||
|
|
volumes:
|
||
|
|
- data:/data
|
||
|
|
- media:/data/media
|
||
|
|
ports:
|
||
|
|
- 9447:8008/tcp # Reverse Proxy Should point http to this port
|
||
|
|
|
||
|
|
entrypoint:
|
||
|
|
- sh
|
||
|
|
- -c
|
||
|
|
- |
|
||
|
|
if [ ! -f /data/homeserver.yaml ]; then
|
||
|
|
/start.py generate;
|
||
|
|
|
||
|
|
REG_SECRET=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||
|
|
echo "registration_shared_secret: \"$$REG_SECRET\"" >> /data/homeserver.yaml
|
||
|
|
|
||
|
|
# Add public_baseurl
|
||
|
|
echo "public_baseurl: https://matrix.karsttech.com" >> /data/homeserver.yaml
|
||
|
|
|
||
|
|
echo "" >> /data/homeserver.yaml
|
||
|
|
echo "email:" >> /data/homeserver.yaml
|
||
|
|
echo " smtp_host: $$SMTP_HOST" >> /data/homeserver.yaml
|
||
|
|
echo " smtp_port: $$SMTP_PORT" >> /data/homeserver.yaml
|
||
|
|
echo " smtp_user: $$SMTP_USER" >> /data/homeserver.yaml
|
||
|
|
echo " smtp_pass: $$SMTP_PASS" >> /data/homeserver.yaml
|
||
|
|
echo " require_transport_security: true" >> /data/homeserver.yaml
|
||
|
|
echo " notif_from: \"$$SMTP_FROM\"" >> /data/homeserver.yaml
|
||
|
|
echo " enable_notifs: true" >> /data/homeserver.yaml
|
||
|
|
fi;
|
||
|
|
|
||
|
|
/start.py run &
|
||
|
|
SYNAPSE_PID=$$!
|
||
|
|
|
||
|
|
if [ ! -f /data/.admin_created ]; then
|
||
|
|
echo 'Waiting for Synapse to start...'
|
||
|
|
sleep 5
|
||
|
|
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||
|
|
if curl -s http://localhost:8008/_matrix/client/versions > /dev/null 2>&1; then
|
||
|
|
echo 'Synapse is ready, creating admin user...'
|
||
|
|
if register_new_matrix_user -u $$SYNAPSE_ADMIN_USER -p $$SYNAPSE_ADMIN_PASSWORD -a -c /data/homeserver.yaml http://localhost:8008; then
|
||
|
|
touch /data/.admin_created
|
||
|
|
echo 'Admin user created successfully!'
|
||
|
|
else
|
||
|
|
echo 'ERROR: Failed to create admin user!'
|
||
|
|
fi
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
echo "Waiting... attempt $$i/15"
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
else
|
||
|
|
echo 'Admin user already created (skipping)'
|
||
|
|
fi
|
||
|
|
|
||
|
|
wait $$SYNAPSE_PID
|
||
|
|
|
||
|
|
element:
|
||
|
|
image: vectorim/element-web:latest
|
||
|
|
restart: unless-stopped
|
||
|
|
user: root
|
||
|
|
environment:
|
||
|
|
ELEMENT_SERVER_NAME: matrix.karsttech.com
|
||
|
|
ELEMENT_BASE_URL: https://matrix.karsttech.com
|
||
|
|
ports:
|
||
|
|
- 9449:8080
|
||
|
|
entrypoint: |
|
||
|
|
sh -c '
|
||
|
|
cat > /tmp/config.json <<EOF
|
||
|
|
{
|
||
|
|
"default_server_config": {
|
||
|
|
"m.homeserver": {
|
||
|
|
"base_url": "$$ELEMENT_BASE_URL",
|
||
|
|
"server_name": "$$ELEMENT_SERVER_NAME"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"brand": "Element",
|
||
|
|
"disable_guests": false,
|
||
|
|
"disable_3pid_login": false,
|
||
|
|
"default_theme": "light",
|
||
|
|
"room_directory": {
|
||
|
|
"servers": ["$$ELEMENT_SERVER_NAME", "matrix.org"]
|
||
|
|
},
|
||
|
|
"enable_presence_by_default": true,
|
||
|
|
"setting_defaults": {
|
||
|
|
"breadcrumbs": true
|
||
|
|
},
|
||
|
|
"show_labs_settings": false
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
cp /tmp/config.json /app/config.json
|
||
|
|
exec nginx -g "daemon off;"
|
||
|
|
'
|
||
|
|
|
||
|
|
volumes:
|
||
|
|
media:
|
||
|
|
data:
|
||
|
|
```
|
||
|
|
|
||
|
|
## How It Works
|
||
|
|
|
||
|
|
The magic happens in the custom entrypoint script. Let's break down what's happening:
|
||
|
|
|
||
|
|
### First Run Detection
|
||
|
|
|
||
|
|
```sh
|
||
|
|
if [ ! -f /data/homeserver.yaml ]; then
|
||
|
|
```
|
||
|
|
|
||
|
|
We check if the homeserver configuration exists. If it doesn't, we know this is a fresh installation and we need to generate everything.
|
||
|
|
|
||
|
|
### Configuration Generation
|
||
|
|
|
||
|
|
```sh
|
||
|
|
/start.py generate;
|
||
|
|
|
||
|
|
REG_SECRET=$$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||
|
|
echo "registration_shared_secret: \"$$REG_SECRET\"" >> /data/homeserver.yaml
|
||
|
|
```
|
||
|
|
|
||
|
|
We run the standard Synapse configuration generator, then immediately append our registration secret. The secret is randomly generated from `/dev/urandom` to ensure it's unique and secure.
|
||
|
|
|
||
|
|
### Configuration Injection
|
||
|
|
|
||
|
|
Rather than manually editing the generated `homeserver.yaml` later, we append our custom configuration directly:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
echo "public_baseurl: https://matrix.karsttech.com" >> /data/homeserver.yaml
|
||
|
|
|
||
|
|
echo "" >> /data/homeserver.yaml
|
||
|
|
echo "email:" >> /data/homeserver.yaml
|
||
|
|
echo " smtp_host: $$SMTP_HOST" >> /data/homeserver.yaml
|
||
|
|
# ... more email config
|
||
|
|
```
|
||
|
|
|
||
|
|
This pulls values from environment variables, making the setup portable and easy to version control.
|
||
|
|
|
||
|
|
### Background Server Start
|
||
|
|
|
||
|
|
```
|
||
|
|
/start.py run &
|
||
|
|
SYNAPSE_PID=$$!
|
||
|
|
```
|
||
|
|
|
||
|
|
We start Synapse in the background and capture its process ID. This lets us continue with initialization while the server warms up.
|
||
|
|
|
||
|
|
### Admin User Creation
|
||
|
|
|
||
|
|
```sh
|
||
|
|
if [ ! -f /data/.admin_created ]; then
|
||
|
|
echo 'Waiting for Synapse to start...'
|
||
|
|
sleep 5
|
||
|
|
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||
|
|
if curl -s http://localhost:8008/_matrix/client/versions > /dev/null 2>&1; then
|
||
|
|
```
|
||
|
|
|
||
|
|
We use a marker file (`.admin_created`) to track whether we've already created the admin user. Then we poll the Synapse API endpoint until it responds, indicating the server is ready to accept user registration.
|
||
|
|
|
||
|
|
Once ready, we create the admin user and touch the marker file so we don't try to create it again on subsequent restarts.
|
||
|
|
|
||
|
|
### Process Management
|
||
|
|
|
||
|
|
```
|
||
|
|
wait $SYNAPSE_PID
|
||
|
|
```
|
||
|
|
|
||
|
|
Finally, we wait on the Synapse process. This ensures the container doesn't exit and that Docker properly handles signals when you stop the container.
|
||
|
|
|
||
|
|
## Element Web Client
|
||
|
|
|
||
|
|
The Element service is simpler but uses the same pattern - we generate its configuration from environment variables on startup:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
cat > /tmp/config.json <<EOF
|
||
|
|
{
|
||
|
|
"default_server_config": {
|
||
|
|
"m.homeserver": {
|
||
|
|
"base_url": "$$ELEMENT_BASE_URL",
|
||
|
|
"server_name": "$$ELEMENT_SERVER_NAME"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
...
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
cp /tmp/config.json /app/config.json
|
||
|
|
```
|
||
|
|
|
||
|
|
This lets you change Element's configuration by updating environment variables rather than mounting configuration files.
|
||
|
|
|
||
|
|
## Federation and .well-known
|
||
|
|
|
||
|
|
For [matrix] federation to work properly, you need to serve the `.well-known` endpoints. If you're using HAProxy like I am, here's the configuration:
|
||
|
|
|
||
|
|
```
|
||
|
|
acl matrix_wellknown_client_path_acl var(txn.txnpath) -m str -i /.well-known/matrix/client
|
||
|
|
acl matrix_wellknown_server_path_acl var(txn.txnpath) -m str -i /.well-known/matrix/server
|
||
|
|
http-request return status 200 content-type application/json string '{"m.homeserver":{"base_url":"https://matrix.karsttech.com"}}' if matrix_wellknown_client_path_acl aclcrt_karsttech_ssl_offloading_frontend
|
||
|
|
http-request return status 200 content-type application/json string '{"m.server":"matrix.karsttech.com:443"}' if matrix_wellknown_server_path_acl aclcrt_karsttech_ssl_offloading_frontend
|
||
|
|
```
|
||
|
|
|
||
|
|
These rules tell other [matrix] servers where to find your homeserver and ensure clients can auto-discover your server configuration.
|
||
|
|
|
||
|
|
## Deployment
|
||
|
|
|
||
|
|
To deploy this setup:
|
||
|
|
|
||
|
|
1. Replace `matrix.karsttech.com` with your domain
|
||
|
|
2. Set your admin credentials in `SYNAPSE_ADMIN_USER` and `SYNAPSE_ADMIN_PASSWORD`
|
||
|
|
3. Configure your SMTP settings for email notifications
|
||
|
|
4. Run `docker compose up -d`
|
||
|
|
|
||
|
|
That's it. The first startup will take a bit longer as it generates configuration and creates the admin user, but subsequent restarts will be fast since everything is already initialized.
|
||
|
|
|
||
|
|
## A Quick Warning
|
||
|
|
|
||
|
|
If you are new to [matrix] I highly recommend that the first thing you do when you set up a new account is to go to **Settings** -> **Encryption** and save / write down your Recovery Key! Without this if you clear your browser cache or move to a new computer you will [lose access](https://element.io/blog/resetting-the-server-side-key-backup/) to your encrypted messages and will need to reestablish trust with your fellow [matrix] users.
|
||
|
|
|
||
|
|
## A Small Invitation
|
||
|
|
|
||
|
|
As a special way to reach out to any readers who also use [matrix] chat, the first 10 people to use [THIS LINK](https://element.karsttech.com/#/register?token=MMqg3UbvlCuhZ.tH) will be able to create an account at *matrix.karsttech.com*
|
||
|
|
|
||
|
|
Happy self-hosting!
|