Skip to main content

Matrix Secure Messaging

Shenzhen, China

Setup

Create a work directory and set the necessary permissions:

mkdir -p /opt/matrix/synapse/{data,certs,vhost,html,db}
chown -R 991:991 /opt/matrix
chown -R 999:999 /opt/matrix/synapse/db
# chmod -R 777 /opt/matrix

Then we need an internal docker network for the services to communicate on:

docker network create matrix

Set up the Reverse Proxy

NGINX Proxy Service

jwilder/nginx-proxy nginx-proxy sets up a container running nginx and docker-gen. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.

nano /opt/matrix/docker-compose.yml
services:
proxy:
image: "jwilder/nginx-proxy"
container_name: "proxy"
volumes:
- "certs:/etc/nginx/certs"
- "vhost:/etc/nginx/vhost.d"
- "html:/usr/share/nginx/html"
- "/run/docker.sock:/tmp/docker.sock:ro"
- "/opt/matrix/synapse-federation:/etc/nginx/vhost.d/time.instar.com/synapse-federation"
networks: ["matrix"]
restart: "always"
ports:
- "80:80"
- "443:443"

networks:
matrix:
external: true

volumes:
certs:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: /opt/matrix/synapse/certs
o: bind
vhost:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: /opt/matrix/synapse/vhost
o: bind
html:
driver: local # Define the driver and options under the volume name
driver_opts:
type: none
device: /opt/matrix/synapse/html
o: bind

Letsencrypt Proxy Companion Service

letsencrypt-nginx-proxy-companion:

nano /opt/matrix/docker-compose.yml
services:
proxy:

...

letsencrypt:
image: "jrcs/letsencrypt-nginx-proxy-companion"
container_name: "letsencrypt"
volumes:
- "certs:/etc/nginx/certs"
- "vhost:/etc/nginx/vhost.d"
- "html:/usr/share/nginx/html"
- "/run/docker.sock:/var/run/docker.sock:ro"
environment:
NGINX_PROXY_CONTAINER: "proxy"
networks: ["matrix"]
restart: "always"
depends_on: ["proxy"]

...

Run the compose file with:

sudo apt-get update
sudo apt-get install docker-compose-plugin
docker compose version
Docker Compose version v2.24.7
docker compose -f /opt/matrix/docker-compose.yml up -d
curl localhost
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>nginx</center>
</body>
</html>

Set up Synapse

Configuration

nano /opt/matrix/synapse/docker-compose.yml
services:
synapse:
image: "matrixdotorg/synapse:latest"
restart: always
container_name: "synapse"
volumes:
- "/opt/matrix/synapse/data:/data"
environment:
# Replace this with your domain
VIRTUAL_HOST: "sub.domain.com"
VIRTUAL_PORT: 8008
# Replace this with your domain
LETSENCRYPT_HOST: "sub.domain.com"
# Replace this with your domain
SYNAPSE_SERVER_NAME: "sub.domain.com"
SYNAPSE_REPORT_STATS: "yes"
networks: ["matrix"]


networks:
matrix:
external: true

Generate the configuration file:

docker compose -f /opt/matrix/synapse/docker-compose.yml run --rm synapse generate

This will generate the config file inside ./data, named homeserver.yaml. We need to make the following changes:

  • The server_name variable is set to the subdomain of your choice, as set in the environment variable SYNAPSE_SERVER_NAME.
  • TLS is set to false. You are using a reverse proxy, so TLS is handled through your web server. Leave the port be.
  • Make sure enable_registration is set to true, so that you can sign up and use your homeserver.
nano /opt/matrix/synapse/data/homeserver.yaml
# Configuration file for Synapse.
#
# This is a YAML file: see [1] for a quick introduction. Note in particular
# that *indentation is important*: all the elements of a list or dictionary
# should have the same indentation.
#
# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
#
# For more information on how to configure Synapse, including a complete accounting of
# each option, go to docs/usage/configuration/config_documentation.md or
# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
## Server ##
server_name: "sub.domain.com"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: sqlite3
args:
database: /data/homeserver.db
log_config: "/data/sub.domain.com.log.config"
media_store_path: /data/media_store
registration_shared_secret: secretkey
report_stats: true
macaroon_secret_key: secretkey
form_secret: secretkey
signing_key_path: "/data/sub.domain.com.signing.key"
trusted_key_servers:
- server_name: "matrix.org"


# vim:ft=yaml

And make sure that all necessary ports are open on your server - e.g. with ufw:

# ufw allow 8448/tcp
ufw allow http
ufw allow https

Now that everything is in place, you can start synapse using a command as simple as:

docker compose -f /opt/matrix/synapse/docker-compose.yml up -d

Matrix Messenger Synapse

If the container crashes immediately check the container logs:

docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
5e5c18fcf2f8 matrixdotorg/synapse:latest "/start.py" 19 seconds ago Exited (1)

In this case the mounted in configuration file was not readable by the container due to a permission issue:

docker logs 5e5c18fcf2f8
Starting synapse with args -m synapse.app.homeserver --config-path /data/homeserver.yaml

Error in configuration at 'signing_key':
Error accessing file '/data/sub.domain.com.signing.key':
[Errno 13] Permission denied: '/data/sub.domain.com.signing.key'

Adding PostgreSQL

By default, synapse uses SQLite for its database. For a more important usecase, I recommend using PostgreSQL instead - if you need it, simply add it to your compose file:

nano /opt/matrix/synapse/docker-compose.yml
version: "3"

services:
synapse:

...

postgresql:
image: postgres:latest
container_name: synapsedb
restart: always
environment:
POSTGRES_PASSWORD: secretdbpassword
POSTGRES_USER: synapse
POSTGRES_DB: synapse
POSTGRES_INITDB_ARGS: "--encoding='UTF8' --lc-collate='C' --lc-ctype='C'"
volumes:
- "/opt/matrix/synapse/db:/var/lib/postgresql/data"
networks: ["matrix"]

...

networks:
matrix:
external: true

Configure Synapse

nano /opt/matrix/synapse/data/homeserver.yaml

And replace the SQLite configuration in:

nano /opt/matrix/synapse/data/homeserver.yaml
## Server ##
server_name: "sub.domain.com"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
# name: sqlite3
# args:
# database: /data/homeserver.db
name: psycopg2
args:
user: synapse
password: secretdbpassword
database: synapse
host: synapsedb
cp_min: 5
cp_max: 10
log_config: "/data/sub.domain.com.log.config"
media_store_path: /data/media_store
registration_shared_secret: secretkey
report_stats: true
macaroon_secret_key: secretkey
form_secret: secretkey
signing_key_path: "/data/sub.domain.com.signing.key"
trusted_key_servers:
- server_name: "matrix.org"


# vim:ft=yaml

The name of the database is psycopg2, which is a PostgreSQL adapter for python. Now reload the compose file:

docker compose -f /opt/matrix/synapse/docker-compose.yml down
docker compose -f /opt/matrix/synapse/docker-compose.yml up -d

Verify that the container is now using the Postgres Database:

docker exec -ti synapsedb psql -d synapse -U synapse

psql (16.2 (Debian 16.2-1.pgdg120+2))
Type "help" for help.

synapse=# \l
List of databases
Name | Owner | Encoding | Access privileges
-----------+---------+----------+---------------------
postgres | synapse | UTF8 |
synapse | synapse | UTF8 |
template0 | synapse | UTF8 | =c/synapse +
| | | synapse=CTc/synapse
template1 | synapse | UTF8 | =c/synapse +
| | | synapse=CTc/synapse

synapse=# \c synapse
You are now connected to database "synapse" as user "synapse".
synapse=# \dt
List of relations
Schema | Name | Type | Owner
--------+------------------------------------------------+-------+---------
public | access_tokens | table | synapse
public | account_data | table | synapse
public | account_validity | table | synapse
public | application_services_state | table | synapse
...

Test the Synapse Matrix Homeserver

Our Synapse Server now allows us to use the Matrix protocol to exchange messages. But we still need to install a client to interact with our server. A list of optional clients can be found here.

Matrix Messenger Synapse

Add your server address instead of the official server network and create an account - done!

Setting up Federation in Synapse

Federation is basically the ability to communicate with users on a different homeserver.

By default, each matrix server tries to reach another matrix server via port 8443. The following process basically tells the other servers to use a different port. Because https is already working in port 443, you're simply going to delegate the default matrix communication port to 443.

Configuration File for NGINX

nano /opt/matrix/synapse-federation
location /.well-known/matrix/server {
return 200 '{"m.server": "sub.domain.com:443"}';
}

Change sub.domain.com to your domain.

Edit the Docker Compose File

Open your docker-compose.yml file and add another entry to the volumes array:

nano /opt/matrix/docker-compose.yml
services:
proxy:
image: "jwilder/nginx-proxy"
container_name: "proxy"
volumes:
- "/opt/matrix/synapse/certs:/etc/nginx/certs"
- "/opt/matrix/synapse/vhost:/etc/nginx/vhost.d"
- "/opt/matrix/synapse/html:/usr/share/nginx/html"
- "/run/docker.sock:/tmp/docker.sock:ro"
- /opt/matrix/synapse-federation:/etc/nginx/vhost.d/sub.domain.com/synapse-federation
networks: ["matrix"]
restart: "always"
ports:
- "80:80"
- "443:443"

Change sub.domain.com to your domain. Now reload the compose file:

docker compose -f /opt/matrix/docker-compose.yml down
docker compose -f /opt/matrix/docker-compose.yml up -d

Testing

Run the curl command and you should receive a true as response (make sure you have jq installed):

curl https://federationtester.matrix.org/api/report?server_name=sub.domain.com --silent | jq -r '.FederationOK'
true

Change sub.domain.com to your domain.

Deactivate Registrations

Once everyone joined shut of the registration option to your server:

nano /opt/matrix/synapse/data/homeserver.yaml
## Registration ##
enable_registration: false

And cycle the synapse service to take effect!

Configuration

The server starts up with user registration deactivated:

Matrix Messenger Synapse

We can now use the admin API to create a user:

docker exec -ti synapse register_new_matrix_user

usage: register_new_matrix_user [-h] [-u USER] [-p PASSWORD] [-t USER_TYPE] [-a | --no-admin] (-c CONFIG | -k SHARED_SECRET) [server_url]
register_new_matrix_user: error: one of the arguments -c/--config -k/--shared-secret is required

The shared secret was generated for us in the home server configuration. Which is mounted to /data/homeserver.yaml inside the container. Also the server URL is set to use port 8008:

docker exec -ti synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008

New user localpart [root]: admin
Password: secretpassword
Confirm password: secretpassword
Make admin [no]: yes
Sending registration request...
Success!

The user can now be used to log into your server:

Matrix Messenger Synapse

Matrix Messenger Synapse