The following ports have to be opened in your firewall:
80/tcp # HTTP 443/tcp # HTTPS
You will have to decide if you want Synapse to use your root domain or a subdomain, and on which subdomain you want to serve Element.
Examples are:
quietlife.nl as the homeserver domain and chat.quietlife.nl serving Elementmatrix.quietlife.nl as the homeserver domain and chat.quietlife.nl serving Elementchat.quietlife.nl both as the homeserver domain and the domain serving Elementquietlife.nl as the homeserver domain without serving Element
It doesn't really matter which you choose, but configuration will depend on it slightly. In this case, I decided to use quietlife.nl as the homeserver domain and chat.quietlife.nl to serve Element.
Note that Element is merely a web application talking to Synapse. It is not required to build a Matrix server. You can also use Synapse without Element, using locally installed Matrix client applications instead. So you are free to leave out Element entirely if you wish.
Unfortunately, Synapse and Element are not packaged for Debian, so you can't simply apt install them. For now, Docker is likely the easiest way to deploy this stack.
It is probably a good idea to first read Setting up IPv6 NAT in Docker in order to have IPv6 working for these containers.
First create /var/opt/matrix:
sudo mkdir /var/opt/matrix
Then create docker-compose.yml there:
services: postgres: image: postgres:17 restart: always environment: POSTGRES_USER: "synapse" POSTGRES_PASSWORD: "5yN@Ps3" POSTGRES_DB: "synapse" healthcheck: test: ["CMD", "pg_isready", "-U", "synapse"] networks: - network volumes: - /var/opt/matrix/postgres:/var/lib/postgresql/data synapse: image: matrixdotorg/synapse:latest restart: always depends_on: - postgres networks: - network ports: - "127.0.0.1:8008:8008" - "[::1]:8008:8008" volumes: - /var/opt/matrix/synapse:/data
Make sure to change POSTGRES_PASSWORD to something you generated yourself.
If you also want to serve Element, add this:
element: image: vectorim/element-web:latest restart: always networks: - network ports: - "127.0.0.1:8088:80" - "[::1]:8088:80" volumes: - /var/opt/matrix/element/config.json:/app/config.json
If you have an IPv6-enabled Docker setup, add this:
networks: network: driver: bridge enable_ipv6: true ipam: driver: default config: - subnet: 2001:db8:db8:8008::/64
Start PostgreSQL:
cd /var/opt/matrix sudo docker compose up -d postgres
The database created by PostgreSQL's init script upsets Synapse because of the collation.
So after starting PostgreSQL, drop the database and recreate it manually:
sudo docker exec -it matrix-postgres-1 dropdb -U synapse synapse sudo docker exec -it matrix-postgres-1 createdb -U synapse -E UTF8 -l C -T template0 synapse
For the Synapse container, create its data directory:
sudo mkdir /var/opt/matrix/synapse
Create its configuration file there:
server_name: "quietlife.nl" report_stats: false media_store_path: "/data/media_store" signing_key_path: "/data/signing.key" form_secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" macaroon_secret_key: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" registration_shared_secret: "cccccccccccccccccccccccccccccccccccccccccccccccccc" listeners: - port: 8008 tls: false type: http x_forwarded: true resources: - names: [client, federation] compress: false database: name: psycopg2 args: host: "postgres" port: 5432 user: "synapse" password: "5yN@Ps3" database: "synapse" trusted_key_servers: - server_name: "matrix.org"
Make sure you change server_name and password under database.
For form_secret, macaroon_secret_key and registration_shared_secret you have to generate random password strings.
For example:
apg -m50 -n3
If you want to add e-mail support to Synapse, append this to homeserver.yaml:
email: smtp_host: "quietlife.nl" smtp_port: 587 smtp_user: "synapse@quietlife.nl" smtp_pass: "PASSWORD" require_transport_security: true enable_notifs: true notif_from: "Quiet Chat <synapse@quietlife.nl>" app_name: "Quiet Chat" client_base_url: "https://chat.quietlife.nl"
You will, of course, have to change the majority of fields to match your own mail setup and whether or not you also host Element.
Set the permissions to 991:991:
sudo chown -R 991:991 /var/opt/matrix/synapse
And start Synapse:
cd /var/opt/matrix sudo docker compose up -d synapse
(Feel free to skip this part if you do not want to host Element. You can still use the Element desktop application or other Matrix clients if you don't.)
For the Element container, create its data directory:
sudo mkdir /var/opt/matrix/element
Create its configuration file there:
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://quietlife.nl"
}
},
"disable_custom_urls": true,
"disable_3pid_login": true,
"disable_guests": true,
"brand": "Quiet Chat",
"branding": {
"auth_footer_links": [],
"welcome_background_url": "https://quietlife.nl/resources/images/background.jpg"
},
"embedded_pages": {
"login_for_welcome": true
},
"setting_defaults": {
"UIFeature.registration": false,
"UIFeature.passwordReset": false
}
}
And start Element:
cd /var/opt/matrix sudo docker compose up -d element
A reverse proxy will be needed to do the TLS termination and to forward incoming traffic to the Docker containers.
For that, install nginx:
sudo apt install nginx certbot python3-certbot-nginx
Generate certificates with certbot for your homeserver domain and - if you want to host Element - for your Element domain:
sudo certbot certonly -d quietlife.nl sudo certbot certonly -d chat.quietlife.nl
In this case, I decided to use separate certificates for both, but using a SAN for chat. would also work.
(Skip this step for the domains for which you already have certificates.)
For your homeserver domain, it's important to do two things:
/.well-known/matrix/[client|server]/_matrix and /_synapse/client to SynapseThere is no problem if you host another site on your root domain, as long as these three subpaths can be used for Matrix.
You have to define where clients and federating servers should connect to using two simple JSON arrays. It is probably easiest to put that in the nginx configuration rather than separate files in your web root.
location /.well-known/matrix/client { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.homeserver":{"base_url":"https://quietlife.nl"}}'; } location /.well-known/matrix/server { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.server":"quietlife.nl:443"}'; }
Additionally, you'll want to route Matrix traffic to Synapse rather than to your regular website:
location ~ ^(/_matrix|/_synapse/client) { client_max_body_size 50M; proxy_http_version 1.1; proxy_pass http://localhost:8008; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; }
An example of a full configuration file, with a regular website hosted at https://quietlife.nl/ would look like this:
server { listen 80 default_server; listen [::]:80 default_server; listen 443 ssl default_server; listen [::]:443 ssl default_server; http2 on; server_name quietlife.nl www.quietlife.nl; ssl_certificate /etc/letsencrypt/live/quietlife.nl/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/quietlife.nl/privkey.pem; access_log /var/log/nginx/quietlife.nl-access.log; error_log /var/log/nginx/quietlife.nl-error.log; root /var/www/quietlife.nl; index index.html index.php; if ($scheme != "https") { return 301 https://$host$request_uri; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; } location /.well-known/matrix/client { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.homeserver":{"base_url":"https://quietlife.nl"}}'; } location /.well-known/matrix/server { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.server":"quietlife.nl:443"}'; } location ~ ^(/_matrix|/_synapse/client) { client_max_body_size 50M; proxy_http_version 1.1; proxy_pass http://localhost:8008; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }
Make sure the site is enabled:
cd /etc/nginx/sites-enabled sudo ln -s ../sites-available/quietlife.nl . sudo systemctl reload nginx.service
If you want to host Element, you'll most likely want that on a subdomain, such as chat.quietlife.nl.
Of course, skip this step if you don't run the Element container.
server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; http2 on; server_name chat.quietlife.nl; access_log /var/log/nginx/chat.quietlife.nl-access.log; error_log /var/log/nginx/chat.quietlife.nl-error.log; ssl_certificate /etc/letsencrypt/live/chat.quietlife.nl/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/chat.quietlife.nl/privkey.pem; if ($scheme != "https") { return 301 https://$host$request_uri; } location / { add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block"; proxy_pass http://localhost:8088; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }
Make sure the site is enabled:
cd /etc/nginx/sites-enabled sudo ln -s ../sites-available/chat.quietlife.nl . sudo systemctl reload nginx.service
If you chose to use the same domain for your homeserver and for Element, you can use a single configuration file. In this example, everything would be hosted on chat.quietlife.nl:
server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; http2 on; server_name chat.quietlife.nl; access_log /var/log/nginx/chat.quietlife.nl-access.log; error_log /var/log/nginx/chat.quietlife.nl-error.log; ssl_certificate /etc/letsencrypt/live/chat.quietlife.nl/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/chat.quietlife.nl/privkey.pem; if ($scheme != "https") { return 301 https://$host$request_uri; } location /.well-known/matrix/client { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.homeserver":{"base_url":"https://chat.quietlife.nl"}}'; } location /.well-known/matrix/server { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type" always; add_header Access-Control-Allow-Credentials true always; default_type application/json; return 200 '{"m.server":"chat.quietlife.nl:443"}'; } location ~ ^(/_matrix|/_synapse/client) { client_max_body_size 50M; proxy_http_version 1.1; proxy_pass http://localhost:8008; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } location / { add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header X-XSS-Protection "1; mode=block"; proxy_pass http://localhost:8088; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }
Make sure the site is enabled:
cd /etc/nginx/sites-enabled sudo ln -s ../sites-available/chat.quietlife.nl . sudo systemctl reload nginx.service
If you, for some reason, use Apache2 as a reverse proxy server, the configuration is of course completely different.
For the JSON files in /.well-known/:
ProxyPass /.well-known/ !
Alias /.well-known/matrix/client /var/www/quietlife.nl/.well-known/matrix/client.json
Alias /.well-known/matrix/server /var/www/quietlife.nl/.well-known/matrix/server.json
<Location "/.well-known/">
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization,Content-Type"
Header always set Access-Control-Allow-Credentials "true"
</Location>
Then create those two JSON files:
{"m.homeserver":{"base_url":"https://quietlife.nl"}}
{"m.server":"quietlife.nl:443"}
For proxying Matrix traffic to Synapse:
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
AllowEncodedSlashes NoDecode
LimitRequestBody 52428800
ProxyPreserveHost on
ProxyPass /_matrix http://localhost:8008/_matrix nocanon
ProxyPassReverse /_matrix http://localhost:8008/_matrix
ProxyPass /_synapse/client http://localhost:8008/_synapse/client nocanon
ProxyPassReverse /_synapse/client http://localhost:8008/_synapse/client
Make sure the site is enabled:
sudo a2ensite quietlife.nl.conf sudo systemctl reload apache2.service
For Element:
<VirtualHost *:80>
ServerName chat.quietlife.nl
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName chat.quietlife.nl
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/chat.quietlife.nl/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/chat.quietlife.nl/privkey.pem
CustomLog /var/log/apache2/chat.quietlife.nl-access.log combined
ErrorLog /var/log/apache2/chat.quietlife.nl-error.log
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
ProxyPass / http://localhost:8088/
ProxyPassReverse / http://localhost:8088/
RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s
RequestHeader set X-Forwarded-Proto https
RequestHeader set Host %{HTTP_HOST}s
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
</VirtualHost>
Make sure the site is enabled:
sudo a2ensite chat.quietlife.nl.conf sudo systemctl reload apache2.service
It's probably a good idea to make regular backups of the Synapse database. You can use this script:
#!/bin/bash backup_directory="/var/backups/synapse_backups" timestamp=$(date +"%Y-%m-%d_%H:%M") # Create backup directory if not present if [ ! -d $backup_directory ]; then mkdir -p $backup_directory; fi # Create a database dump and compress it nice -n 19 docker exec matrix-postgres-1 pg_dump postgres://synapse@localhost:5432/synapse | nice -n 19 xz -T1 -1 > $backup_directory/synapse-postgres-$timestamp.sql.xz # Delete backups older than one week find $backup_directory -mtime +7 -exec rm {} \; exit 0
Make it executable:
sudo chmod +x /usr/local/bin/backup-synapse.sh
And run it as a cronjob for the root user:
# Run hourly backups of Synapse 30 * * * * /usr/local/bin/backup-synapse.sh
Because Synapse and Element run in Docker, you can't update them with unattended-upgrades.
You can use Watchtower to automate container updates, with this line in root's crontab:
# Check for container updates every day at 21:00 0 21 * * * /usr/bin/docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once --cleanup
The quickest way to create user accounts is to talk to Synapse directly on http://localhost:8008.
Within the container, there is a register_new_matrix_user command:
docker exec -it matrix-synapse-1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
Eventually, you'll probably want to use synadm instead.
Unlike Synapse, this is packaged in Debian, and also talks to Synapse over http://localhost:8008, which is bound to the host.