How to Deploy 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 article explains how to run a self-hosted chat server on an Ubuntu 22.04 server. After completing the steps, you 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.
Prerequisites
Before you begin, you need to:
- Have access to an Ubuntu 22.04 server with at least 2 GB of RAM and one vCPU core as a non-root user with sudo privileges.
- Update the installed packages on the server.
- Create the DNS A records
matrix.example.com,element.example.com, andcoturn.example.compointing to your server's public IP address.
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.
Allow HTTP traffic.
console$ sudo ufw allow http
Allow HTTPS traffic.
console$ sudo ufw allow https
Allow the Matrix federation port.
console$ sudo ufw allow 8448
Review the active rules.
console$ sudo ufw status
The output displays
80,443, and8448with anALLOWaction.
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.
Download the repository signing key.
console$ sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
Add the Matrix repository and bind it to the keyring.
console$ 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
Update the package index.
console$ sudo apt update
Install Synapse.
console$ sudo apt install matrix-synapse-py3
The installer prompts for a server name. Enter your Matrix domain name, such as
example.com. EnterNto decline reporting of anonymized statistics.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 theNote/etc/matrix-synapse/conf.d/server_name.yamlfile.
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.
Install PostgreSQL.
console$ sudo apt install postgresql postgresql-contrib
Open the PostgreSQL shell.
console$ sudo -u postgres psql
Create the Synapse database role. Replace
DB-PASSWORDwith a strong password.psqlpostgres=# CREATE ROLE synapse LOGIN PASSWORD 'DB-PASSWORD';
The role is created and can log in with the password you set.
Create the Synapse database owned by that role.
psqlpostgres=# CREATE DATABASE synapsedb OWNER synapse LOCALE 'C' ENCODING 'UTF8' TEMPLATE template0;
The database is created and owned by the Synapse role. Synapse refuses to start against a database created with any other collation.
Exit the shell.
psqlpostgres=# \q
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.
Download the Nginx signing key.
console$ curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg > /dev/null
Add the Nginx repository.
console$ 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
Verify that the repository file exists.
console$ cat /etc/apt/sources.list.d/nginx.list
The output displays the repository line. An empty result means the file was not written, and
aptinstalls the older Ubuntu package instead.Update the package index.
console$ sudo apt update
Install Nginx.
console$ sudo apt install nginx
Start Nginx.
console$ sudo systemctl start nginx
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.
Install Certbot and the Nginx plugin.
console$ sudo apt install certbot python3-certbot-nginx
Verify the installed version.
console$ certbot --version
The output displays the Certbot version.
Issue the certificate for the Matrix subdomain. Replace
name@example.comwith your email address andmatrix.example.comwith your Matrix subdomain.console$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m name@example.com -d matrix.example.com
Generate a Diffie-Hellman parameter file.
console$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096
The command takes several minutes to complete.
Verify that automatic renewal works.
console$ sudo certbot renew --dry-run
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.
Create the database configuration file.
console$ sudo nano /etc/matrix-synapse/conf.d/database.yaml
Add the following configuration. Replace
DB-PASSWORDwith the password you set in Install and Configure PostgreSQL.yamldatabase: name: psycopg2 args: user: synapse password: 'DB-PASSWORD' database: synapsedb host: localhost cp_min: 5 cp_max: 10
Save and close the file.
name: psycopg2: Selects the PostgreSQL driver instead of the default SQLite driver.cp_minandcp_max: Set the minimum and maximum size of the database connection pool.
Generate a registration shared secret.
console$ 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
Restart Synapse so that it connects to PostgreSQL and loads the shared secret.
console$ sudo systemctl restart matrix-synapse
Verify that Synapse is running.
console$ sudo systemctl status matrix-synapse
Verify that the output reports
Active: active (running). Synapse creates its schema insynapsedbon this first start, which takes up to a minute.Create an administrator account. Enter a username and password when prompted, then type
yesto grant administrator rights.console$ register_new_matrix_user -c /etc/matrix-synapse/conf.d/registration_shared_secret.yaml http://localhost:8008
The output displays
Success!when the account is created.Create a registration configuration file to allow public sign-ups.
console$ sudo nano /etc/matrix-synapse/conf.d/registration.yaml
Add the following configuration to enable registration with email verification. Replace
SMTP-PASSWORDwith the password for the sending mailbox, and the remaining mail server values with your own.yamlenable_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: 'noreply@example.com' 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 <noreply@example.com>"
To skip verification instead, replace the
registrations_require_3pidandemailblocks with the following line.yamlenable_registration_without_verification: true
Save and close the file.
Create a presence configuration file.
console$ sudo nano /etc/matrix-synapse/conf.d/presence.yaml
Add the following configuration.
yamlpresence: enabled: false
Save and close the file.
Synapse tracks each user's online status by default, which raises CPU usage on small servers. Disabling presence removes that overhead.
Restart Synapse to apply the changes.
console$ sudo systemctl restart matrix-synapse
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.
Open the main Nginx configuration file.
console$ sudo nano /etc/nginx/nginx.conf
Add the following directive inside the
httpblock, before theinclude /etc/nginx/conf.d/*.conf;line.iniserver_names_hash_bucket_size 64;Save and close the file.
Create the Synapse site configuration.
console$ sudo nano /etc/nginx/conf.d/synapse.conf
Add the following configuration. Replace
matrix.example.comwith your Matrix subdomain.ini# 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; } }
Save and close the file.
http2 on;: Enables HTTP/2 for the server block. Nginx 1.25.1 and later deprecate the olderlisten ... http2form and log a warning for every listener that uses it.client_max_body_size: Raises the upload limit from the 1 MB Nginx default so that media uploads succeed.
Test the configuration syntax.
console$ sudo nginx -t
The output displays
syntax is okandtest is successful.Restart Nginx.
console$ sudo systemctl restart nginx
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.
Install Coturn.
console$ sudo apt install coturn
Allow the TURN control ports.
console$ sudo ufw allow 3478
Allow the TURN TLS port.
console$ sudo ufw allow 5349
Allow the media relay port range.
console$ sudo ufw allow 49152:65535/udp
Issue a certificate for the Coturn subdomain. Replace
coturn.example.comwith your Coturn subdomain.console$ sudo certbot certonly --nginx -d coturn.example.com
Back up the default configuration file.
console$ sudo mv /etc/turnserver.conf /etc/turnserver.conf.bak
Generate an authentication secret and write it to a new configuration file.
console$ echo "static-auth-secret=$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)" | sudo tee /etc/turnserver.conf
The command prints the generated secret. Copy the value, because Synapse needs it later in this section.
Open the Coturn configuration file.
console$ sudo nano /etc/turnserver.conf
Add the following configuration below the authentication secret. Replace
coturn.example.comwith your Coturn subdomain.iniuse-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
Save and close the file.
use-auth-secret: Enables the shared-secret authentication mode that Synapse expects.denied-peer-ip: Blocks relaying to private and reserved address ranges, which prevents the TURN server from reaching internal services.
Restart Coturn.
console$ sudo systemctl restart coturn
Create the Synapse TURN configuration file.
console$ sudo nano /etc/matrix-synapse/conf.d/turn.yaml
Add the following configuration. Replace
YOUR-STATIC-AUTH-SECRETwith thestatic-auth-secretvalue from/etc/turnserver.conf, andcoturn.example.comwith your Coturn subdomain.yamlturn_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
Save and close the file.
Restart Synapse to apply the configuration.
console$ sudo systemctl restart matrix-synapse
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.
Open a Matrix client such as the Element web app, or install the desktop or mobile app. Other options are listed in the Matrix client directory.
Select Sign in, then edit the homeserver address and enter your Matrix subdomain.
https://matrix.example.comSign in with the administrator account you created in Configure Synapse.
Create a secure backup for your encrypted messages using a security key or passphrase when the client prompts you.
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.
Install the JSON processor used to read the release metadata.
console$ sudo apt install jq
Create the web root for Element.
console$ sudo mkdir -p /var/www/element
Change to the directory.
console$ cd /var/www/element
Store the latest release tag in a variable.
console$ latest="$(curl -s https://api.github.com/repos/element-hq/element-web/releases/latest | jq -r .tag_name)"
Verify that the variable holds a version tag.
console$ echo "$latest"
The output displays a tag such as
v1.12.25. An empty value ornullmeans the request failed, and the download in the next step produces a broken filename.Download the release archive.
console$ sudo wget https://github.com/element-hq/element-web/releases/download/${latest}/element-${latest}.tar.gz
Extract the archive.
console$ sudo tar xf element-${latest}.tar.gz
Link the extracted directory to a stable path.
console$ sudo ln -s element-${latest} current
Repoint the link when you upgrade Element later, after downloading and extracting the new archive. Replace
NEW-VERSIONwith the new release tag.console$ sudo ln -nfs element-NEW-VERSION current
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.
Change to the
currentdirectory.console$ cd current
Create the configuration file from the sample.
console$ sudo cp config.sample.json config.json
Open the configuration file.
console$ sudo nano config.json
Edit the default homeserver settings to point at your own server.
json"m.homeserver": { "base_url": "https://matrix.example.com", "server_name": "example.com" },
m.homeserver: Nested under the default server configuration block near the top of the file.base_url: The address Element connects to, which is your Matrix subdomain.server_name: The server name you entered when installing Synapse.
Change the brand name to customize the page title.
json"brand": "My Example Chat",
Set
disable_gueststo prevent guest access.json"disable_guests": true,
Save and close the file.
Issue a certificate for the Element subdomain. Replace
element.example.comwith your Element subdomain.console$ sudo certbot certonly --nginx -d element.example.com
Create the Element site configuration.
console$ sudo nano /etc/nginx/conf.d/element.conf
Add the following configuration. Replace
element.example.comwith your Element subdomain.iniserver { 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; }
Save and close the file.
Test the configuration syntax.
console$ sudo nginx -t
The output displays
syntax is okandtest is successful.Restart Nginx.
console$ sudo systemctl restart nginx
Open your Element subdomain in a web browser and sign in with your Matrix account.
https://element.example.com
Conclusion
You have deployed a self-hosted Matrix chat server on Ubuntu 22.04, with Synapse storing data in PostgreSQL, Nginx serving client and federation traffic over HTTPS, Coturn relaying voice and video calls, and a self-hosted Element web client signing in against your own homeserver. For further configuration such as federation tuning, media retention, and worker processes, see the Synapse documentation and the Element web configuration reference.