Sh
Applications Azure

Synapse Homeserver on Ubuntu 24.04 on Azure User Guide

| Product: Synapse Homeserver on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of a Synapse homeserver on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Synapse is the reference server implementation of the Matrix protocol, an open standard for secure, decentralised real time communication. It holds your organisation's messages, rooms and uploaded media on infrastructure inside your own Azure subscription, and it can either federate with the wider public Matrix network or run entirely private.

The image is a complete single node stack rather than just the server process. Alongside Synapse it ships a browser based Matrix web client, so users can sign in and start talking without installing anything, and coturn, a TURN and STUN relay, so voice and video calls still connect when both participants sit behind restrictive firewalls or carrier grade NAT. PostgreSQL is the datastore, and nginx terminates TLS as the single public entry point.

Nothing in this image ships with a known secret. Synapse's cryptographic identity, its signing key, is generated on the first boot of every VM, and so are the PostgreSQL password, the registration_shared_secret, the macaroon_secret_key, the form_secret, the shared secret between the homeserver and the TURN relay, the TLS certificate and a unique administrator password. Two VMs created from this image are cryptographically distinct servers. Open registration is closed by default, so nobody can create an account on your server without an invitation. Backed by 24/7 cloudimg support.

Matrix and the Matrix logo are trademarks of The Matrix.org Foundation C.I.C. Element is a registered trademark of Element Creations Ltd. coturn is an independent open source project. This image is produced by cloudimg and is not affiliated with, sponsored by, or endorsed by The Matrix.org Foundation, Element Creations Ltd, or the coturn project. It ships the free and open source software unmodified: Synapse and the web client under the GNU AGPL v3 (the web client is additionally offered under the GNU GPL v3), and coturn under the BSD 3-Clause licence. Corresponding sources are at https://github.com/element-hq/synapse, https://github.com/element-hq/element-web and https://github.com/coturn/coturn.

The postgresql, matrix-synapse, coturn and nginx services all active, with the pinned Synapse, coturn and web client versions and a 200 from the Matrix client API

What is included:

  • Synapse 1.160.0 — installed from the official packages.matrix.org repository, not the much older package in Ubuntu's own archive, and run unmodified under systemd
  • Element Web 1.12.27 — the open source Matrix web client, served as static files from the same TLS listener, shipped unmodified
  • coturn 4.18.0 — built from the upstream release because Ubuntu 24.04 still carries 4.6.1, and hardened so the relay cannot be used to reach your private network
  • PostgreSQL 16 — the homeserver database, bound to loopback with no host port published
  • nginx — the single public listener, terminating TLS for the web client, the client API and federation
  • A dedicated media volume — uploads, avatars and the remote media cache live on their own resizable Azure data disk, not on the OS disk
  • First boot secret generation — signing key, database password, registration_shared_secret, macaroon_secret_key, form_secret, TURN shared secret, TLS certificate and administrator password, all unique to each VM

Before you start: your server name is permanent

This is the one decision on this page that cannot be undone later, so it comes first.

Every Matrix homeserver has a server name. It is not just an address: it is baked into every user ID (@alice:example.com), every room ID and every event signature your server ever produces. It cannot be changed once people have started using the server. Changing it means starting a new homeserver.

The image handles this in the way that is safest by default and still lets you do the right thing:

  • If you do nothing, the first boot sets the server name to the VM's public IP address. The appliance works immediately and is perfectly usable for evaluation, an internal deployment reached by IP, or a proof of concept.
  • If you want a real domain — which you almost certainly do for anything permanent, and which you need for federation — set it before the first boot, by passing cloud-init custom data that writes the name into /etc/cloudimg/matrix-stack/server-name. See "Using your own domain" below.
  • If you have already booted and nobody has signed up yet, matrix-set-server-name will re-provision the server under a new name. It is destructive by design and tells you whether it is still safe to run.

Prerequisites

  • An Azure subscription with permission to create virtual machines
  • An SSH key pair for VM access
  • A VM size of Standard_B2ms or larger (2 vCPU, 8 GiB RAM). Synapse, PostgreSQL and coturn together need more memory than a 4 GiB size can give them without swapping, and the image deliberately ships no swap
  • Inbound network security group rules for the ports listed under "Step 6"
  • Optionally, a DNS name you control, if you want a real domain and federation

Step 1: Deploy from the Azure Portal

  1. In the Azure Portal, search the Marketplace for Synapse Homeserver on Ubuntu 24.04 LTS and select the cloudimg offer.
  2. Choose Standard_B2ms or larger.
  3. Set the administrator username to azureuser and upload your SSH public key.
  4. On the Networking tab, allow inbound SSH (22) and HTTPS (443) to begin with. Add the call and federation ports from Step 6 before you invite users.
  5. Review and create.

Step 2: Deploy from the Azure CLI

az vm create \
  --resource-group my-matrix-rg \
  --name my-synapse-vm \
  --image cloudimg:synapse-homeserver-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Step 3: Connect to your VM

ssh azureuser@<vm-ip>

Step 4: Confirm the services are running

The stack is four services. matrix-stack-firstboot is a one shot unit that runs once, on the very first boot, and mints everything this VM needs before the homeserver is allowed to start.

systemctl is-active postgresql matrix-synapse coturn nginx

Expected output:

active
active
active
active

If you connect within the first minute or so of a cold boot, you may find the web address answering HTTP 503. That is deliberate: nginx refuses to serve anything until first boot has finished writing this VM's own secrets, so there is no window in which a half configured homeserver is reachable. Watch it complete with:

sudo systemctl status matrix-stack-firstboot.service --no-pager

Confirm the Matrix client API is answering through nginx:

curl -ks -o /dev/null -w '%{http_code}\n' https://127.0.0.1/_matrix/client/versions

Expected output:

200

Step 5: Retrieve your per instance credentials

Every VM generates its own administrator on first boot. The credentials are written to a root only file:

sudo cat /root/matrix-stack-credentials.txt

This file contains:

  • matrix.server.name — this homeserver's permanent server name
  • matrix.web.url — the address to open in a browser
  • matrix.admin.user / matrix.admin.pass / matrix.admin.mxid — the per instance administrator
  • matrix.federation.port — the port other homeservers use to reach yours
  • matrix.turn.realm — the realm the TURN relay authenticates against

The per VM credentials file is root only 0600, listing the key names it holds, alongside the per VM signing key and the first boot sentinel

The homeserver's signing key, at /etc/matrix-synapse/homeserver.signing.key, is this server's cryptographic identity. It is generated on this VM and exists nowhere else. Back it up with your database, and never copy it to another server.

Step 6: Understand the security model and open the right ports

Rather than checking that the ports you expect are open, list every socket bound to something other than loopback, so anything unexpected has nowhere to hide:

sudo ss -lntuH | awk '{print $1, $5}' | grep -vE '127\.0\.0|\[::1\]' | sort -u

The only sockets bound off loopback are SSH, nginx on 80, 443 and 8448, and coturn on 3478 and 5349, with PostgreSQL and Synapse reachable only on 127.0.0.1

That is the complete public surface, and it is what your network security group needs to allow:

Port Protocol Purpose
22 TCP SSH administration
80 TCP Redirects to HTTPS, and serves ACME http-01 if you install a trusted certificate
443 TCP The web client, the Matrix client API and .well-known discovery
8448 TCP Matrix federation. Leave this closed if you want a private, non federating server
3478 TCP and UDP TURN and STUN, for call setup and relay
5349 TCP and UDP The same over TLS and DTLS, for networks that only permit HTTPS-like traffic
49152-49231 UDP The bounded media relay range coturn allocates calls from, one port per live call leg

Everything else stays on loopback. PostgreSQL and the Synapse process itself are never reachable from off the box:

sudo ss -lntH | grep -E ':(5432|8008)' | awk '{print $1, $4}'

Expected output:

LISTEN 127.0.0.1:5432
LISTEN 127.0.0.1:8008

Open registration is closed, so a stranger who finds your server cannot create an account on it:

curl -ks -o /dev/null -w '%{http_code}\n' -X POST https://127.0.0.1/_matrix/client/v3/register \
  -H 'Content-Type: application/json' -d '{"username":"walkup","password":"not-allowed"}'

Expected output:

403

Confirm the per instance administrator password really authenticates, and that a wrong one does not:

ADMIN_USER=$(sudo grep '^matrix.admin.user=' /root/matrix-stack-credentials.txt | cut -d= -f2-)
ADMIN_PASS=$(sudo grep '^matrix.admin.pass=' /root/matrix-stack-credentials.txt | cut -d= -f2-)
GOOD=$(curl -ks -o /dev/null -w '%{http_code}' -X POST https://127.0.0.1/_matrix/client/v3/login \
  -H 'Content-Type: application/json' \
  -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${ADMIN_USER}\"},\"password\":\"${ADMIN_PASS}\"}")
BAD=$(curl -ks -o /dev/null -w '%{http_code}' -X POST https://127.0.0.1/_matrix/client/v3/login \
  -H 'Content-Type: application/json' \
  -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${ADMIN_USER}\"},\"password\":\"wrong-password-xyz\"}")
case "${BAD}" in 403|429) VERDICT="rejected (${BAD})" ;; *) VERDICT="UNEXPECTED (${BAD})" ;; esac
echo "correct password -> ${GOOD}; wrong password -> ${VERDICT}"

Expected output:

correct password -> 200; wrong password -> rejected (403)

Synapse rate limits sign in attempts, so a run of failed logins starts returning 429 instead of 403. Both mean the credential was refused; 429 simply means Synapse stopped answering in detail because it was being hammered. That protection is on by default and is why the check above accepts either.

Step 7: Sign in to the web client

Open https://<vm-ip>/ in a browser. The image ships a self signed certificate generated for this VM, so your browser will warn on the first visit; accept it, or install a trusted certificate as described below.

Sign in with the administrator username and password from Step 5.

The web client sign in page, showing the homeserver this image is configured against

After signing in you land on an empty account on your own homeserver.

The signed in web client on a freshly deployed homeserver, with the room list on the left

A note on the first sign in on any additional device. Matrix verifies devices against each other so that end to end encrypted history can be shared safely. The first device to sign in to an account sets up that account's encryption identity by itself. Every device after that is asked to confirm itself against one you already trust, and if you no longer have one, the client offers a "Can't confirm?" path that resets the identity and starts again. That is normal Matrix behaviour, not a fault with the image.

Step 8: Create a room and send a message

Use the + button beside the room list to create a room, invite people to it, and type into the composer at the bottom. Private rooms are end to end encrypted by default, so the server stores only ciphertext for them.

A room on this homeserver with messages sent from the browser, end to end encrypted, showing the delivery tick

Your account settings show the full Matrix ID your homeserver issued you, in the form @user:server-name.

The account settings panel showing the administrator's Matrix ID on this homeserver

Step 9: Confirm voice and video calling is wired up

Calls between people on restrictive networks are relayed by coturn. Synapse hands each signed in client a short lived TURN credential, derived from a secret it shares with the relay, so there is no static call login to leak:

ADMIN_USER=$(sudo grep '^matrix.admin.user=' /root/matrix-stack-credentials.txt | cut -d= -f2-)
ADMIN_PASS=$(sudo grep '^matrix.admin.pass=' /root/matrix-stack-credentials.txt | cut -d= -f2-)
# Synapse rate limits sign ins, so back off and retry rather than failing outright if
# you have just run the checks above.
for attempt in $(seq 1 12); do
  TOKEN=$(curl -ks -X POST https://127.0.0.1/_matrix/client/v3/login \
    -H 'Content-Type: application/json' \
    -d "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"${ADMIN_USER}\"},\"password\":\"${ADMIN_PASS}\"}" \
    | python3 -c 'import json,sys; print(json.load(sys.stdin).get("access_token",""))')
  [ -n "${TOKEN}" ] && break
  sleep 10
done
[ -n "${TOKEN}" ] || { echo "could not sign in to read TURN settings — Synapse is still rate limiting; wait a few minutes"; exit 1; }
curl -ks -H "Authorization: Bearer ${TOKEN}" https://127.0.0.1/_matrix/client/v3/voip/turnServer \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print("ttl:", d["ttl"]); [print("uri:", u) for u in d["uris"]]'

Expected output (the URIs carry your own server name):

ttl: 86400
uri: turn:<SERVER_NAME>:3478?transport=udp
uri: turn:<SERVER_NAME>:3478?transport=tcp
uri: turns:<SERVER_NAME>:5349?transport=tcp

The homeserver issuing a short lived TURN credential, the TLS relay answering on 5349, and the relay refusing to forward traffic to the instance metadata address

The relay is deliberately restricted: it refuses to forward traffic to loopback, to private ranges, or to the Azure instance metadata address at 169.254.169.254. Without that restriction an authenticated user could point the relay at your own network and read instance metadata through it.

Step 10: Add users

Registration is closed, so you create accounts explicitly. This uses the registration_shared_secret that first boot generated for this VM:

sudo /opt/venvs/matrix-synapse/bin/register_new_matrix_user \
  -c /etc/matrix-synapse/conf.d/90-cloudimg-instance.yaml \
  -u alice -p 'choose-a-strong-password' --no-admin http://127.0.0.1:8008 \
  || echo "That username is already registered on this homeserver — choose another."

Add --admin instead of --no-admin to create another server administrator.

To let people sign up for themselves instead, set enable_registration: true in /etc/matrix-synapse/conf.d/10-cloudimg.yaml, pair it with a verification method or a registration token so you are not running an open relay for spam, and restart with sudo systemctl restart matrix-synapse. Synapse also supports SAML, CAS and OpenID Connect single sign on; configure your provider in the same directory.

Using your own domain

Check whether it is still safe to change the server name:

sudo matrix-set-server-name --check

On a freshly deployed VM this reports that only the first boot administrator exists and that a change is still safe.

The best way is to never boot with the wrong name at all. Pass cloud-init custom data at deployment time so the name is in place before first boot runs:

#cloud-config
write_files:
  - path: /etc/cloudimg/matrix-stack/server-name
    permissions: '0644'
    content: |
      matrix.your-domain.example

If the VM is already running and nobody has signed up yet, re-provision it. This destroys the database, the signing key and every account and creates a new administrator, because a Matrix server name cannot be renamed in place:

sudo matrix-set-server-name matrix.your-domain.example --yes
sudo cat /root/matrix-stack-credentials.txt

Point an A record for that name at the VM's public IP, then install a trusted certificate so browsers and other homeservers accept it:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d matrix.your-domain.example

Certbot writes the certificate into the nginx configuration. Point coturn at the same files by editing cert= and pkey= in /etc/coturn/turnserver.conf, then sudo systemctl restart coturn.

Serving the homeserver from a shorter user ID

If you want IDs like @alice:your-domain.example while the server itself runs at matrix.your-domain.example, that is what the .well-known delegation this image already serves is for. The image publishes /.well-known/matrix/server and /.well-known/matrix/client on the VM; to use the shorter form you must set the server name to the short domain before first boot and serve those two documents from the web server at that short domain. Doing it afterwards is the destructive path above.

Federation

Federation is what lets your users talk to people on other Matrix servers. It needs port 8448 open, a real domain, and a certificate other servers will trust. Check what your server advertises:

curl -ks https://127.0.0.1/.well-known/matrix/server

For a private, internal only deployment, simply leave 8448 closed in your network security group. Everything else keeps working; your users just cannot reach or be reached by other homeservers.

Media storage

Uploads, avatars and cached remote media live on a dedicated Azure data disk mounted at /var/lib/matrix-synapse/media_store, so they can grow without touching the OS disk and can be resized independently:

df -h /var/lib/matrix-synapse/media_store

Backups

Back up three things together: the PostgreSQL database, the signing key, and the media store. The signing key is the one that cannot be regenerated: without it, a restored server is a different server.

sudo -u postgres pg_dump synapse | gzip > /var/backups/synapse-$(date +%F).sql.gz
sudo cp /etc/matrix-synapse/homeserver.signing.key /var/backups/
sudo tar czf /var/backups/media-$(date +%F).tar.gz -C /var/lib/matrix-synapse media_store

Support

This image is published and supported by cloudimg with 24/7 support. Contact support@cloudimg.co.uk with your Azure subscription ID and the VM name. Security updates for the operating system are applied automatically by unattended-upgrades; Synapse itself is updated from the official repository with sudo apt-get update && sudo apt-get install --only-upgrade matrix-synapse-py3.