Matrix is an open protocol for decentralized, end-to-end encrypted chat, and Synapse is its most widely used homeserver. Element Web is the reference Matrix client that runs in the browser. In this guide you will install Synapse on Ubuntu 24.04 from the official Matrix.org repository, store its data in PostgreSQL, publish it behind Nginx with Let's Encrypt certificates, set up federation with .well-known delegation, host Element Web, and create your first admin user.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 GB of RAM (4 GB if you expect many users or large public rooms).
  • A non-root user with sudo privileges.
  • A domain name, referred to here as your_domain, with DNS A records pointing to your server for:
    • your_domain (serves the .well-known delegation files),
    • matrix.your_domain (Synapse),
    • element.your_domain (Element Web).

With this layout, user IDs look like @alice:your_domain while Synapse runs on matrix.your_domain. If your_domain is already hosted on another server, you can serve the two small .well-known files from there instead (see Step 5).

Step 1 - Installing Synapse

The Matrix.org team publishes Synapse packages for Ubuntu 24.04. They are more current than the Ubuntu archive package, which the Synapse project does not recommend.

Install the prerequisites and add the signed repository:

sudo apt update
sudo apt install lsb-release wget apt-transport-https
sudo install -m 0755 -d /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
echo "deb [signed-by=/etc/apt/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

Install the package:

sudo apt update
sudo apt install matrix-synapse-py3

The installer asks two questions:

  1. Name of the server: enter your_domain (not matrix.your_domain).
  2. Report anonymous statistics: choose according to your preference.

The package stores the server name in /etc/matrix-synapse/conf.d/server_name.yaml, generates a signing key and starts Synapse on 127.0.0.1:8008. Check that it responds:

sudo systemctl status matrix-synapse --no-pager
curl -s http://localhost:8008/_matrix/client/versions | head -c 120; echo
{"versions":["r0.0.1","r0.1.0","r0.2.0","r0.3.0","r0.4.0","r0.5.0","r0.6.0","r0.6.1","v1.1","v1.2",...

Step 2 - Switching the database to PostgreSQL

Out of the box, Synapse uses SQLite, which is only suitable for testing. Switch to PostgreSQL now, before any users or rooms exist, so you do not have to migrate data later.

Install PostgreSQL:

sudo apt install postgresql

Create a database user. The command prompts for a password; choose a strong one and keep it for the next file:

sudo -u postgres createuser --pwprompt synapse_user

Create the database. Synapse requires the C locale and UTF-8 encoding:

sudo -u postgres createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse_user synapse

Configuration files in /etc/matrix-synapse/conf.d/ override homeserver.yaml and survive package upgrades without prompts. Create one for the database:

sudo nano /etc/matrix-synapse/conf.d/database.yaml
database:
  name: psycopg2
  args:
    user: synapse_user
    password: your_db_password
    dbname: synapse
    host: localhost
    cp_min: 5
    cp_max: 10

Replace your_db_password with the password you just set. The file contains a secret, so make it readable only by Synapse:

sudo chown matrix-synapse: /etc/matrix-synapse/conf.d/database.yaml
sudo chmod 600 /etc/matrix-synapse/conf.d/database.yaml

Restart Synapse. On startup it creates its tables in PostgreSQL:

sudo systemctl restart matrix-synapse
sudo journalctl -u matrix-synapse -n 20 --no-pager

Look for lines that mention psycopg2 and Synapse now listening on TCP port 8008. Confirm the tables exist:

sudo -u postgres psql -d synapse -c '\dt' | head -n 5
                        List of relations
 Schema |              Name               | Type  |    Owner
--------+---------------------------------+-------+--------------
 public | access_tokens                   | table | synapse_user

Step 3 - Setting the public URL and registration secret

Synapse needs to know the public HTTPS address clients use to reach it, and a shared secret lets you create users from the command line while public registration stays disabled.

Generate a random secret:

openssl rand -hex 32

Create another override file:

sudo nano /etc/matrix-synapse/conf.d/homeserver-custom.yaml
public_baseurl: "https://matrix.your_domain/"
enable_registration: false
registration_shared_secret: "paste_the_generated_secret_here"
max_upload_size: 50M

Protect it and restart Synapse:

sudo chown matrix-synapse: /etc/matrix-synapse/conf.d/homeserver-custom.yaml
sudo chmod 600 /etc/matrix-synapse/conf.d/homeserver-custom.yaml
sudo systemctl restart matrix-synapse
sudo systemctl is-active matrix-synapse
active

Step 4 - Installing Nginx and opening the firewall

Nginx terminates TLS and forwards Matrix traffic to Synapse on localhost. Install it together with Certbot and its Nginx plugin:

sudo apt install nginx certbot python3-certbot-nginx

Allow SSH, HTTP and HTTPS through UFW. Federation also runs over port 443 thanks to the delegation you will set up, so port 8448 does not need to be opened:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Step 5 - Configuring Nginx for Synapse and delegation

Create a server block for matrix.your_domain that proxies the Matrix client and federation APIs:

sudo nano /etc/nginx/sites-available/matrix.your_domain
server {
    listen 80;
    listen [::]:80;
    server_name matrix.your_domain;

    location ~ ^(/_matrix|/_synapse/client) {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        client_max_body_size 50M;
        proxy_http_version 1.1;
    }
}

Next, create a server block for your_domain that publishes the two delegation documents. /.well-known/matrix/server tells other homeservers where to federate, and /.well-known/matrix/client tells clients where the client API lives:

sudo nano /etc/nginx/sites-available/your_domain
server {
    listen 80;
    listen [::]:80;
    server_name your_domain;

    location = /.well-known/matrix/server {
        default_type application/json;
        return 200 '{"m.server": "matrix.your_domain:443"}';
    }

    location = /.well-known/matrix/client {
        default_type application/json;
        add_header Access-Control-Allow-Origin "*";
        return 200 '{"m.homeserver": {"base_url": "https://matrix.your_domain"}}';
    }
}

If your_domain is already served elsewhere, skip this file and publish the same two JSON documents at those paths on the existing site, with the Access-Control-Allow-Origin: * header on the client file.

Enable both sites, test the configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/matrix.your_domain /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Request certificates. Certbot adds the HTTPS listeners to both server blocks and sets up an HTTP-to-HTTPS redirect when asked:

sudo certbot --nginx -d matrix.your_domain -d your_domain

Verify both endpoints over HTTPS:

curl -s https://matrix.your_domain/_matrix/federation/v1/version; echo
curl -s https://your_domain/.well-known/matrix/server; echo
{"server":{"name":"Synapse","version":"1.xxx.0"}}
{"m.server": "matrix.your_domain:443"}

Finally, open https://federationtester.matrix.org/, enter your_domain and check that the report shows Federation OK.

Step 6 - Hosting Element Web

Element Web is a static web application: you download a release, add a small config.json and serve it with Nginx.

Install jq to read the GitHub API response, then download the latest release:

sudo apt install jq
ELEMENT_VERSION=$(curl -fsSL https://api.github.com/repos/element-hq/element-web/releases/latest | jq -r .tag_name)
echo "$ELEMENT_VERSION"
wget "https://github.com/element-hq/element-web/releases/download/${ELEMENT_VERSION}/element-${ELEMENT_VERSION}.tar.gz"

Extract it under /var/www and point a stable symlink at the versioned directory, which makes future upgrades a matter of switching the link:

sudo tar -xzf "element-${ELEMENT_VERSION}.tar.gz" -C /var/www/
sudo ln -sfn "/var/www/element-${ELEMENT_VERSION}" /var/www/element

Create the configuration file:

sudo nano /var/www/element/config.json
{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.your_domain",
            "server_name": "your_domain"
        }
    },
    "disable_guests": true,
    "room_directory": {
        "servers": ["your_domain", "matrix.org"]
    }
}

Validate the JSON:

jq . /var/www/element/config.json > /dev/null && echo "config.json is valid"

Create the Nginx server block for Element:

sudo nano /etc/nginx/sites-available/element.your_domain
server {
    listen 80;
    listen [::]:80;
    server_name element.your_domain;

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

    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header Content-Security-Policy "frame-ancestors 'self'";

    location / {
        try_files $uri $uri/ =404;
    }
}

Enable it and request a certificate:

sudo ln -s /etc/nginx/sites-available/element.your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d element.your_domain

Open https://element.your_domain. You should see the Element sign-in page with your homeserver preselected.

Step 7 - Creating the first admin user

Public registration is disabled, so create accounts with register_new_matrix_user. It reads the shared secret from the file you created in Step 3 and prompts for the user name, password and whether the user is an admin:

sudo register_new_matrix_user -c /etc/matrix-synapse/conf.d/homeserver-custom.yaml http://localhost:8008
New user localpart [root]: alice
Password:
Confirm password:
Make admin [no]: yes
Sending registration request...
Success!

Sign in at https://element.your_domain as alice with the password you set. Your full Matrix ID is @alice:your_domain. Run the same command again, answering no to the admin question, for each regular user.

When you first sign in, Element offers to set up Secure Backup. Accept it and store the security key somewhere safe: it protects the keys for your end-to-end encrypted messages, and without it you cannot read old encrypted messages on a new device.

Step 8 - Using the Admin API

Admin users can manage the server through the Synapse Admin API. You need an access token: in Element, open Settings, then Help & About, and copy the access token from the Advanced section. Treat it like a password.

Save it in a shell variable for the current session:

read -rs ADMIN_TOKEN

List the first ten users:

curl -s -H "Authorization: Bearer ${ADMIN_TOKEN}" "http://localhost:8008/_synapse/admin/v2/users?from=0&limit=10" | jq '.users[].name'
"@alice:your_domain"
"@bob:your_domain"

Deactivate a user who should no longer have access:

curl -s -X POST -H "Authorization: Bearer ${ADMIN_TOKEN}" -H "Content-Type: application/json" -d '{"erase": false}' "http://localhost:8008/_synapse/admin/v1/deactivate/@bob:your_domain"

The Admin API is reachable on localhost:8008 directly, so you can keep /_synapse/admin off the public Nginx configuration, as it is in this guide.

Troubleshooting

Synapse does not start after a configuration change. YAML indentation errors and wrong database credentials are the usual causes. Read the last lines of the journal:

sudo journalctl -u matrix-synapse -n 50 --no-pager

Federation tester reports errors. Check that https://your_domain/.well-known/matrix/server returns the JSON shown in Step 5 with a valid certificate, and that https://matrix.your_domain/_matrix/federation/v1/version answers from outside the server.

Element shows "Cannot reach homeserver". Check base_url in config.json, and confirm that https://your_domain/.well-known/matrix/client returns the Access-Control-Allow-Origin: * header:

curl -sI https://your_domain/.well-known/matrix/client | grep -i access-control

High memory usage. Synapse caches aggressively. Lower the global cache factor in a conf.d file, for example caches: {global_factor: 0.5}, and restart the service.

Conclusion

You now run your own Matrix homeserver on Ubuntu 24.04, backed by PostgreSQL, published through Nginx with TLS, federated with the rest of the Matrix network and paired with a self-hosted Element Web client. Next, schedule regular PostgreSQL backups with pg_dump together with /etc/matrix-synapse/ (including the signing key) and /var/lib/matrix-synapse/media, consider bridges such as mautrix-telegram to connect other chat networks, and add a TURN server such as coturn so voice and video calls work across restrictive networks.