DEV Community

Cover image for Deploying a Matrix Synapse Chat Server with Element on Ubuntu 22.04
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya Originally published at docs.vultr.com

Deploying a Matrix Synapse Chat Server with Element on Ubuntu 22.04

Matrix is a set of open APIs for decentralized and end-to-end encrypted communication. It works across a collection of federation servers to deliver instant messages, Voice over IP (VoIP), and Internet of Things (IoT) communication in real time. Matrix uses homeservers to store account information and chat history, and federation works like email, so you can either use a server hosted by somebody else or host your own. Synapse is the homeserver implementation maintained by the Matrix.org team, and Element is the most widely used Matrix client. This guide walks through running a self-hosted chat server on an Ubuntu 22.04 server. By the end, you'll have a Synapse homeserver backed by PostgreSQL, served over HTTPS through Nginx, with a Coturn TURN server for voice and video calls and a self-hosted Element web client.

Before you begin, you need an Ubuntu 22.04 server with at least 2 GB of RAM and one vCPU core as a non-root user with sudo privileges, updated packages, and DNS A records for matrix.example.com, element.example.com, and coturn.example.com pointing to your server's public IP address.


1. Configure the Firewall

Synapse serves both client traffic and federation traffic, and each arrives on a different port. Open those ports before installing the packages so that certificate issuance and federation succeed once the services start.

1. Allow HTTP traffic:

$ sudo ufw allow http
Enter fullscreen mode Exit fullscreen mode

2. Allow HTTPS traffic:

$ sudo ufw allow https
Enter fullscreen mode Exit fullscreen mode

3. Allow the Matrix federation port:

$ sudo ufw allow 8448
Enter fullscreen mode Exit fullscreen mode

4. Review the active rules:

$ sudo ufw status
Enter fullscreen mode Exit fullscreen mode

The output displays 80, 443, and 8448 with an ALLOW action.

2. Install Matrix Synapse

Ubuntu does not package Synapse, so the packages come from the official Matrix.org APT repository. Signing the repository with a dedicated keyring restricts that key to this repository alone.

1. Download the repository signing key:

$ sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
Enter fullscreen mode Exit fullscreen mode

2. Add the Matrix repository and bind it to the keyring:

$ echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/matrix-org.list
Enter fullscreen mode Exit fullscreen mode

3. Update the package index:

$ sudo apt update
Enter fullscreen mode Exit fullscreen mode

4. Install Synapse:

$ sudo apt install matrix-synapse-py3
Enter fullscreen mode Exit fullscreen mode

The installer prompts for a server name. Enter your Matrix domain name, such as example.com. Enter N to decline reporting of anonymized statistics.

Note: The server name becomes part of every user ID on the homeserver and is difficult to change after users exist. To change it later, edit the /etc/matrix-synapse/conf.d/server_name.yaml file.

3. Install and Configure PostgreSQL

Synapse uses SQLite by default, which does not perform well enough for a production homeserver. PostgreSQL is the supported production database, and Synapse expects it to be created with a specific locale and character encoding.

1. Install PostgreSQL:

$ sudo apt install postgresql postgresql-contrib
Enter fullscreen mode Exit fullscreen mode

2. Open the PostgreSQL shell:

$ sudo -u postgres psql
Enter fullscreen mode Exit fullscreen mode

3. Create the Synapse database role. Replace DB-PASSWORD with a strong password.

postgres=# CREATE ROLE synapse LOGIN PASSWORD 'DB-PASSWORD';
Enter fullscreen mode Exit fullscreen mode

4. Create the Synapse database owned by that role:

postgres=# CREATE DATABASE synapsedb OWNER synapse LOCALE 'C' ENCODING 'UTF8' TEMPLATE template0;
Enter fullscreen mode Exit fullscreen mode

Synapse refuses to start against a database created with any other collation.

5. Exit the shell:

postgres=# \q
Enter fullscreen mode Exit fullscreen mode

4. Install Nginx

Nginx terminates TLS and proxies client and federation requests to Synapse. Ubuntu 22.04 ships an older Nginx release, so install the current version from the official Nginx repository.

1. Download the Nginx signing key:

$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/null
Enter fullscreen mode Exit fullscreen mode

2. Add the Nginx repository:

$ echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg arch=amd64] http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
Enter fullscreen mode Exit fullscreen mode

3. Verify that the repository file exists:

$ cat /etc/apt/sources.list.d/nginx.list
Enter fullscreen mode Exit fullscreen mode

An empty result means the file was not written, and apt installs the older Ubuntu package instead.

4. Update the package index:

$ sudo apt update
Enter fullscreen mode Exit fullscreen mode

5. Install Nginx:

$ sudo apt install nginx
Enter fullscreen mode Exit fullscreen mode

6. Start Nginx:

$ sudo systemctl start nginx
Enter fullscreen mode Exit fullscreen mode

5. Issue TLS Certificates

Matrix clients and federating servers both require valid TLS. Certbot issues free certificates from Let's Encrypt, and the Nginx plugin handles the HTTP challenge automatically.

1. Install Certbot and the Nginx plugin:

$ sudo apt install certbot python3-certbot-nginx
Enter fullscreen mode Exit fullscreen mode

2. Verify the installed version:

$ certbot --version
Enter fullscreen mode Exit fullscreen mode

3. Issue the certificate for the Matrix subdomain. Replace [email protected] with your email address and matrix.example.com with your Matrix subdomain.

$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d matrix.example.com
Enter fullscreen mode Exit fullscreen mode

4. Generate a Diffie-Hellman parameter file:

$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096
Enter fullscreen mode Exit fullscreen mode

The command takes several minutes to complete.

5. Verify that automatic renewal works:

$ sudo certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

6. Configure Synapse

The package manager overwrites the main Synapse configuration file during updates, so production settings belong in separate files in the drop-in configuration directory. Synapse merges every file in that directory at startup, which keeps your changes safe across upgrades.

1. Create the database configuration file:

$ sudo nano /etc/matrix-synapse/conf.d/database.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration. Replace DB-PASSWORD with the password you set in Install and Configure PostgreSQL.

database:
  name: psycopg2
  args:
    user: synapse
    password: 'DB-PASSWORD'
    database: synapsedb
    host: localhost
    cp_min: 5
    cp_max: 10
Enter fullscreen mode Exit fullscreen mode

Save and close the file. name: psycopg2 selects the PostgreSQL driver instead of the default SQLite driver, and cp_min/cp_max set the minimum and maximum size of the database connection pool.

3. Generate a registration shared secret:

$ echo "registration_shared_secret: '$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)'" | sudo tee /etc/matrix-synapse/conf.d/registration_shared_secret.yaml
Enter fullscreen mode Exit fullscreen mode

4. Restart Synapse so that it connects to PostgreSQL and loads the shared secret:

$ sudo systemctl restart matrix-synapse
Enter fullscreen mode Exit fullscreen mode

5. Verify that Synapse is running:

$ sudo systemctl status matrix-synapse
Enter fullscreen mode Exit fullscreen mode

Verify that the output reports Active: active (running). Synapse creates its schema in synapsedb on this first start, which takes up to a minute.

6. Create an administrator account. Enter a username and password when prompted, then type yes to grant administrator rights.

$ register_new_matrix_user -c /etc/matrix-synapse/conf.d/registration_shared_secret.yaml http://localhost:8008
Enter fullscreen mode Exit fullscreen mode

7. Create a registration configuration file to allow public sign-ups:

$ sudo nano /etc/matrix-synapse/conf.d/registration.yaml
Enter fullscreen mode Exit fullscreen mode

8. Add the following configuration to enable registration with email verification. Replace SMTP-PASSWORD with the password for the sending mailbox, and the remaining mail server values with your own.

enable_registration: true

registrations_require_3pid:
  - email

email:
  smtp_host: mail.example.com
  smtp_port: 587

  # If the mail server has no authentication, skip these two lines
  smtp_user: '[email protected]'
  smtp_pass: 'SMTP-PASSWORD'

  # Optional, require encryption with STARTTLS
  require_transport_security: true

  app_name: 'Example Chat'  # defines value for %(app)s in notif_from and email subject
  notif_from: "%(app)s <[email protected]>"
Enter fullscreen mode Exit fullscreen mode

To skip verification instead, replace the registrations_require_3pid and email blocks with the following line.

enable_registration_without_verification: true
Enter fullscreen mode Exit fullscreen mode

9. Create a presence configuration file:

$ sudo nano /etc/matrix-synapse/conf.d/presence.yaml
Enter fullscreen mode Exit fullscreen mode

10. Add the following configuration:

presence:
  enabled: false
Enter fullscreen mode Exit fullscreen mode

Synapse tracks each user's online status by default, which raises CPU usage on small servers. Disabling presence removes that overhead.

11. Restart Synapse to apply the changes:

$ sudo systemctl restart matrix-synapse
Enter fullscreen mode Exit fullscreen mode

7. Configure Nginx

Synapse listens only on the loopback interface and does not terminate TLS itself. Nginx accepts public traffic, handles TLS, and forwards the Matrix client and federation requests to Synapse.

1. Open the main Nginx configuration file:

$ sudo nano /etc/nginx/nginx.conf
Enter fullscreen mode Exit fullscreen mode

2. Add the following directive inside the http block, before the include /etc/nginx/conf.d/*.conf; line:

server_names_hash_bucket_size 64;
Enter fullscreen mode Exit fullscreen mode

3. Create the Synapse site configuration:

$ sudo nano /etc/nginx/conf.d/synapse.conf
Enter fullscreen mode Exit fullscreen mode

4. Add the following configuration. Replace matrix.example.com with your Matrix subdomain.

# enforce HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name matrix.example.com;
    return 301 https://$host$request_uri;
}

server {
    server_name matrix.example.com;

    # Client port
    listen 443 ssl;
    listen [::]:443 ssl;

    # Federation port
    listen 8448 ssl default_server;
    listen [::]:8448 ssl default_server;

    http2 on;

    access_log  /var/log/nginx/synapse.access.log;
    error_log   /var/log/nginx/synapse.error.log;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/matrix.example.com/chain.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    location ~ ^(/_matrix|/_synapse/client) {
            proxy_pass http://localhost:8008;
            proxy_http_version 1.1;

            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $host;

            # Increase client_max_body_size to match max_upload_size in homeserver.yaml
            client_max_body_size 50M;
    }
}
Enter fullscreen mode Exit fullscreen mode

http2 on; enables HTTP/2 for the server block (Nginx 1.25.1+ deprecates the older listen ... http2 form), and client_max_body_size raises the upload limit from the 1 MB Nginx default so that media uploads succeed.

5. Test the configuration syntax:

$ sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

6. Restart Nginx:

$ sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

8. Install and Configure Coturn

Voice and video calls between clients behind NAT require a Traversal Using Relays around NAT (TURN) server. Coturn relays that media, and Synapse hands out short-lived credentials generated from a shared secret.

1. Install Coturn:

$ sudo apt install coturn
Enter fullscreen mode Exit fullscreen mode

2. Allow the TURN control ports:

$ sudo ufw allow 3478
Enter fullscreen mode Exit fullscreen mode

3. Allow the TURN TLS port:

$ sudo ufw allow 5349
Enter fullscreen mode Exit fullscreen mode

4. Allow the media relay port range:

$ sudo ufw allow 49152:65535/udp
Enter fullscreen mode Exit fullscreen mode

5. Issue a certificate for the Coturn subdomain. Replace coturn.example.com with your Coturn subdomain.

$ sudo certbot certonly --nginx -d coturn.example.com
Enter fullscreen mode Exit fullscreen mode

6. Back up the default configuration file:

$ sudo mv /etc/turnserver.conf /etc/turnserver.conf.bak
Enter fullscreen mode Exit fullscreen mode

7. Generate an authentication secret and write it to a new configuration file:

$ echo "static-auth-secret=$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)" | sudo tee /etc/turnserver.conf
Enter fullscreen mode Exit fullscreen mode

The command prints the generated secret. Copy the value, because Synapse needs it later.

8. Open the Coturn configuration file:

$ sudo nano /etc/turnserver.conf
Enter fullscreen mode Exit fullscreen mode

9. Add the following configuration below the authentication secret. Replace coturn.example.com with your Coturn subdomain.

use-auth-secret
realm=coturn.example.com
cert=/etc/letsencrypt/live/coturn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/coturn.example.com/privkey.pem

# VoIP is UDP, no need for TCP
no-tcp-relay

# Do not allow traffic to private IP ranges
no-multicast-peers
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff

# Limit number of sessions per user
user-quota=12
# Limit total number of sessions
total-quota=1200
Enter fullscreen mode Exit fullscreen mode

use-auth-secret enables the shared-secret authentication mode that Synapse expects, and denied-peer-ip blocks relaying to private and reserved address ranges, preventing the TURN server from reaching internal services.

10. Restart Coturn:

$ sudo systemctl restart coturn
Enter fullscreen mode Exit fullscreen mode

11. Create the Synapse TURN configuration file:

$ sudo nano /etc/matrix-synapse/conf.d/turn.yaml
Enter fullscreen mode Exit fullscreen mode

12. Add the following configuration. Replace YOUR-STATIC-AUTH-SECRET with the static-auth-secret value from /etc/turnserver.conf, and coturn.example.com with your Coturn subdomain.

turn_uris: [ "turn:coturn.example.com?transport=udp", "turn:coturn.example.com?transport=tcp" ]
turn_shared_secret: 'YOUR-STATIC-AUTH-SECRET'
turn_user_lifetime: 86400000
turn_allow_guests: True
Enter fullscreen mode Exit fullscreen mode

13. Restart Synapse to apply the configuration:

$ sudo systemctl restart matrix-synapse
Enter fullscreen mode Exit fullscreen mode

9. Connect a Matrix Client

The homeserver is now reachable over HTTPS, so any Matrix client can sign in to it. Use a hosted client to confirm the deployment before setting up your own Element instance.

1. Open a Matrix client such as the Element web app, or install the desktop or mobile app.

2. Select Sign in, then edit the homeserver address and enter your Matrix subdomain.

https://matrix.example.com
Enter fullscreen mode Exit fullscreen mode

3. Sign in with the administrator account you created in Configure Synapse.

4. Create a secure backup for your encrypted messages using a security key or passphrase when the client prompts you.

10. Install Element

Hosting your own Element instance serves the client from your domain rather than a third-party site. Element ships as a prebuilt archive that Nginx serves as static files.

1. Install the JSON processor used to read the release metadata:

$ sudo apt install jq
Enter fullscreen mode Exit fullscreen mode

2. Create the web root for Element:

$ sudo mkdir -p /var/www/element
Enter fullscreen mode Exit fullscreen mode

3. Change to the directory:

$ cd /var/www/element
Enter fullscreen mode Exit fullscreen mode

4. Store the latest release tag in a variable:

$ latest="$(curl -s https://api.github.com/repos/element-hq/element-web/releases/latest | jq -r .tag_name)"
Enter fullscreen mode Exit fullscreen mode

5. Verify that the variable holds a version tag:

$ echo "$latest"
Enter fullscreen mode Exit fullscreen mode

An empty value or null means the request failed, and the download in the next step produces a broken filename.

6. Download the release archive:

$ sudo wget https://github.com/element-hq/element-web/releases/download/${latest}/element-${latest}.tar.gz
Enter fullscreen mode Exit fullscreen mode

7. Extract the archive:

$ sudo tar xf element-${latest}.tar.gz
Enter fullscreen mode Exit fullscreen mode

8. Link the extracted directory to a stable path:

$ sudo ln -s element-${latest} current
Enter fullscreen mode Exit fullscreen mode

To upgrade Element later, download and extract the new archive, then repoint the link. Replace NEW-VERSION with the new release tag.

$ sudo ln -nfs element-NEW-VERSION current
Enter fullscreen mode Exit fullscreen mode

11. Configure Element

Element reads its settings from a configuration file in the web root. The shipped sample points at the public matrix.org homeserver, so you must change it to your own before the client is usable.

1. Change to the current directory:

$ cd current
Enter fullscreen mode Exit fullscreen mode

2. Create the configuration file from the sample:

$ sudo cp config.sample.json config.json
Enter fullscreen mode Exit fullscreen mode

3. Open the configuration file:

$ sudo nano config.json
Enter fullscreen mode Exit fullscreen mode

4. Edit the default homeserver settings to point at your own server:

"m.homeserver": {
    "base_url": "https://matrix.example.com",
    "server_name": "example.com"
},
Enter fullscreen mode Exit fullscreen mode

base_url is the address Element connects to (your Matrix subdomain), and server_name is the server name you entered when installing Synapse.

5. Change the brand name to customize the page title:

"brand": "My Example Chat",
Enter fullscreen mode Exit fullscreen mode

6. Set disable_guests to prevent guest access:

"disable_guests": true,
Enter fullscreen mode Exit fullscreen mode

7. Issue a certificate for the Element subdomain. Replace element.example.com with your Element subdomain.

$ sudo certbot certonly --nginx -d element.example.com
Enter fullscreen mode Exit fullscreen mode

8. Create the Element site configuration:

$ sudo nano /etc/nginx/conf.d/element.conf
Enter fullscreen mode Exit fullscreen mode

9. Add the following configuration. Replace element.example.com with your Element subdomain.

server {
    listen 80;
    listen [::]:80;
    server_name element.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    http2 on;

    server_name element.example.com;

    root /var/www/element/current;
    index index.html;

    access_log  /var/log/nginx/element.access.log;
    error_log   /var/log/nginx/element.error.log;

    add_header Referrer-Policy "strict-origin" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/element.example.com/chain.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
}
Enter fullscreen mode Exit fullscreen mode

10. Test the configuration syntax:

$ sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

11. Restart Nginx:

$ sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

12. Open your Element subdomain in a web browser and sign in with your Matrix account.

https://element.example.com
Enter fullscreen mode Exit fullscreen mode

Next Steps

  • Tune Synapse worker processes to scale beyond a single-process homeserver.
  • Configure media retention policies to control storage growth over time.
  • Explore federation tuning options in the Synapse documentation.
  • Customize the Element client further using the Element web configuration reference.

For the full guide with additional tips, visit the original article on Vultr Docs.

Top comments (1)

Collapse
 
raknaos profile image
Baptiste Le Bouquin

Solid walkthrough. The port split (client on 443 behind nginx, federation on 8448) is the part people get wrong most often when they follow Docker-based guides — doing it explicitly here is a service to readers.

Two operational notes from running a homeserver: the '.well-known/matrix/client' and '/server' delegations need to be reachable before you test federation, so if you serve Element from a different subdomain the SRV-less path ('matrix.example.com:8448') resolves differently for some implementations. And Coturn tends to be the first thing to OOM on a 2 GB box during call spikes; we ended up capping 'capacity-ratio' and moving to 'postgresql' with 'single_threaded: false' off — the default SQLite path is fine for a demo, painful past ~50 users.

Did you benchmark the 2 GB minimum against a federated room with media, or is that sizing from the docs?