===== Setting up a Synapse + Element Matrix chat server ===== \\ ==== Firewall ==== The following ports have to be opened in your firewall: 80/tcp # HTTP 443/tcp # HTTPS ---- \\ ==== Domain setup ==== 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 Element * ''matrix.quietlife.nl'' as the homeserver domain and ''chat.quietlife.nl'' serving Element * ''chat.quietlife.nl'' both as the homeserver domain and the domain serving Element * ''quietlife.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. ---- \\ ==== Docker ==== 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 [[manuals:networking:docker-ipv6-nat|Setting up IPv6 NAT in Docker]] in order to have IPv6 working for these containers. \\ === Compose file === 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 [[manuals:networking:docker-ipv6-nat|IPv6-enabled Docker setup]], add this: networks: network: driver: bridge enable_ipv6: true ipam: driver: default config: - subnet: 2001:db8:db8:8008::/64 ---- \\ ==== PostgreSQL ==== 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 ---- \\ ==== 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 " 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 ---- \\ ==== Element ==== (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 ---- \\ ==== nginx ==== 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 \\ === SSL certificate creation === 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.) \\ === Homeserver domain === For your homeserver domain, it's important to do two things: * Serve ''/.well-known/matrix/[client|server]'' * Route traffic on ''/_matrix'' and ''/_synapse/client'' to Synapse There 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; } \\ === Full root domain configuration === 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 \\ === Element domain === 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 \\ === Combined Synapse and Element domain === 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 ---- \\ ==== Apache2 equivalents ==== 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 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" 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: ServerName chat.quietlife.nl RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] 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 Make sure the site is enabled: sudo a2ensite chat.quietlife.nl.conf sudo systemctl reload apache2.service ---- \\ ==== Database backups ==== 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 ---- \\ ==== Automatic updates ==== Because Synapse and Element run in Docker, you can't update them with ''unattended-upgrades''.\\ You can use [[https://containrrr.dev/watchtower/|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 ---- \\ ==== Creating user accounts ==== 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 [[https://codeberg.org/synadm/synadm|synadm]] instead. Unlike Synapse, this **is** packaged in Debian, and also talks to Synapse over http://localhost:8008, which is bound to the host. ----