Version: 25.4.17
In the following tutorial we want to install Matrix Synapse including SSL certificates from Letsencrypt with the Nginx Proxy Manager on a VPS on the Internet (in our case at Hetzner.com).
A big thank you goes to Patrick from https://www.cleveradmin.de/ with whom I worked out a great solution!
🛠 Requirements:
– A domain is available with access to the DNS entries
– VPS is set up with root access (Ubuntu)
A small server from e.g. Hetzner.com is sufficient here:

⚠️ Firewall: Please configure the firewall at Hetzner and only allow the following ports:

TCP: 22, 80, 81, 443 (Matrix Synapse, NPM)
TCP: 7881-7882 and UDP 50100 – 50200 are the preparation for Element Call
⚠️ DNS access must also be available for an existing domain:

We start and log in to the server:
ssh root@SERVER-IP
First we make sure that the server is up to date with the following command:
apt-get update && apt-get upgrade && apt-get autoremove
We then restart the server and log in again:
sudo reboot now
We create the folders for our two projects (Synapse and Nginx Proxy Manager -NPM-)
mkdir -p /home/npm
mkdir -p /home/synapse/data
Now we first install Docker by creating a file called “install.sh” in the /home/ directory:
nano /home/install.sh
The file contains the following content (thanks to Patrick Asmus: https://git.techniverse.net/scriptos/public-linux-docker-installer)
#!/bin/bash
# Script Name: docker-installer.v3.sh
# Beschreibung: Docker & Docker-Compose Installer für Ubuntu 22.04 Jammy und Ubuntu 24.04
# Aufruf: bash ./docker-installer.v3.sh
# Autor: Patrick Asmus
# Web: https://www.techniverse.net
# Git-Reposit.: https://git.techniverse.net/scriptos/linux-docker-installer
# Version: 3.4.5
# Datum: 15.11.2024
# Modifikation: change docker-compose version
#####################################################
# Variablen:
COMPOSEVERSION="v2.30.3"
# Betriebssystem und Version prüfen
OS=$(lsb_release -is)
VERSION=$(lsb_release -rs)
if [ "$OS" != "Ubuntu" ] || { [ "$VERSION" != "22.04" ] && [ "$VERSION" != "24.04" ]; }; then
echo "Dieses Script unterstützt nur Ubuntu 22.04 und Ubuntu 24.04. Installation abgebrochen."
exit 1
fi
# Variablen
USER="root"
DOCKER_ROOT_DIR="/var/docker-bin"
COMPOSE_DIR="/home/docker-container"
# Docker installieren
sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
if [ "$VERSION" == "22.04" ]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
elif [ "$VERSION" == "24.04" ]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
fi
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
# Docker-Root-Verzeichnis ändern & Compose Verzeichnis erstellen
sudo systemctl stop docker
sudo mkdir -p $DOCKER_ROOT_DIR
echo "{\"data-root\": \"$DOCKER_ROOT_DIR\"}" | sudo tee /etc/docker/daemon.json > /dev/null
cp /var/lib/docker/* $DOCKER_ROOT_DIR -r
rm -R /var/lib/docker
sudo systemctl start docker
mkdir -p $COMPOSE_DIR
# Docker-compose installieren (gleicher Prozess für beide Versionen)
sudo apt install -y curl
sudo curl -L "https://github.com/docker/compose/releases/download/$COMPOSEVERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Optional: Plugin für Oh my ZSH aktivieren
echo "OhMyZSH Plugin für Docker hinzufügen"
sudo sed -i 's/plugins=(git)/plugins=(git docker)/g' /root/.zshrc
# Überprüfen der Installation
docker --version
docker-compose --version
exit 0
We then execute the file:
cd /home/
bash install.sh
After completing the script, Docker and Docker-Compose are installed.
⚠️ Important: We are now creating a special network in which the individual containers will later operate together:
docker network create --subnet=172.37.51.0/24 matrixnetwork
We start with Matrix Synapse and go to the synapse folder and execute the following command:
cd /home/synapse/
docker run --rm \
-v /home/synapse/data:/data \
-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
-e SYNAPSE_SERVER_NAME=subdomain.deinedomain.com \
-e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:latest generate
⚠️ “subdomain.deinedomain.de” is of course changed beforehand with your own domain. e.g.: matrix.deinedomain.de
The script has now created the so-called homeserver.yaml file in the /synapse/data/ folder.
We open this with :
nano /home/synapse/data/homeserver.yaml
The file should look like this:
server_name: "matrix.deinedomain.com"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
#database:
# name: sqlite3
#args:
# database: /data/homeserver.db
# Datenbank Einstellungen
database:
name: psycopg2
args:
user: synapseuser
password: 7Kn33B655TgV339-777bN
database: synapsedb
host: synapse-db
port: 5432
cp_min: 5
cp_max: 10
log_config: "/data/matrix.deinedomain.com.log.config"
media_store_path: /data/media_store
registration_shared_secret: "N42a1.-58.nB,dkym:N4Jb=b7L+b@cW589HcF&4if~A0iD8e*FQ"
enable_registration: false
report_stats: false
macaroon_secret_key: "XHfX:isQQ44BNbpXcU44pKe55Q+IOH9dfH5SVYAykjnONFzEmp"
form_secret: "&Ld^9od94fYE=mDb,Q8RYaGuJH;##KCCB:AIq3KAL+X23dxE3"
signing_key_path: "/data/matrix.domain.com.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
msc4140_enabled: true
max_event_delay_duration: 24h
rc_message:
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
# Delete all media older than 120 days
media_retention:
local_media_lifetime: 120d
remote_media_lifetime: 120d
#Limit Upload File Size to 250 Megabyte
max_upload_size: 250M
# Activates the .well-known URL config for the Port 443 - is later requiered for Element Call !!
#serve_server_wellknown: true
# vim:ft=yaml
⚠️ It is important to change the database settings as follows:
#database:
# name: sqlite3
#args:
# database: /data/homeserver.db
# Datenbank Einstellungen
database:
name: psycopg2
args:
user: synapseuser
password: 7Kn33B655TgV339-777bN
database: synapsedb
host: synapse-db
port: 5432
cp_min: 5
cp_max: 10
This deactivates the SQL3 database and activates Postgres.
We save the file (CTRL+O) and close the editor again (CTRL+X) and now create the “docker-compose.yml” file in the Synapse folder:
nano /home/synapse/docker-compose.yml
You can use the following Compose file and modify it with your data (change the password for Postgres):
version: "3.9"
services:
synapse-db:
image: postgres:17.4
networks:
matrixnetwork:
ipv4_address: 172.37.51.12
container_name: Synapse-DB
hostname: synapse-db
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "pg_isready", "-q", "-d", "synapsedb", "-U", "synapseuser"]
timeout: 45s
interval: 10s
retries: 10
volumes:
- /home/synapse/db:/var/lib/postgresql/data
environment:
- POSTGRES_DB=synapsedb
- POSTGRES_USER=synapseuser
- POSTGRES_PASSWORD=7Kn33B655TgV339-777bN
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
restart: always
synapse:
image: matrixdotorg/synapse:latest
networks:
matrixnetwork:
ipv4_address: 172.37.51.13
container_name: Synapse
hostname: synapse
security_opt:
- no-new-privileges:true
environment:
- TZ=Europe/Berlin
- SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
volumes:
- /home/synapse/data:/data
ports:
- 8008:8008
restart: always
depends_on:
synapse-db:
condition: service_started
networks:
matrixnetwork:
external: true
Now let’s go back to the project directory and start the container:
cd /home/synapse
docker compose up -d
We can query the result with “docker ps” in the command line. We should see that the containers (Synapse and the database) are running as “Healthy”:

We create the first user (Admin) with the following command:
⚠️ Important: change back to the /home/synapse/ directory!
docker compose exec synapse register_new_matrix_user -u benutzername -p passwort -a -k "geheimes-passwort" http://localhost:8008
Explanation:
-u
: desired user name-p
: password-a
: makes the user an admin (optional)-k
: shared secret fromhomeserver.yaml
http://localhost:8008
: Synapse Admin-API-Endpunkt (Pay attention to correct address, possibly http://synapse:8008 within Compose)
✅ Done: Part 1 is complete. Matrix Synapse is installed. Congratulations! You can verify this by visiting the URL:
http://server-ip:8008

Next step: NPM – Install Nginx Proxy Manager as Docker (SSL certificate / HTTPS)
nano /home/npm/docker-compose.yml
We add the following code to the file:
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
ports:
- '80:80'
- '81:81'
- '443:443'
networks:
matrixnetwork:
ipv4_address: 172.37.51.10
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
restart: always
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
db:
image: 'jc21/mariadb-aria:latest'
networks:
matrixnetwork:
ipv4_address: 172.37.51.11
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
restart: always
volumes:
- ./mysql:/var/lib/mysql
networks:
matrixnetwork:
external: true
Now we’ll ensure that the database is always started alongside NPM at system startup. This doesn’t always work in docker-compose.yml, and you can’t log in to NPM on the interface.
🛠 The solution is a: “systemd-Service”
Create Service File (z. B. /etc/systemd/system/npm-stack.service
):
[Unit]
Description=Docker Compose Nginx Proxy Manager Stack
Requires=docker.service
After=docker.service
[Service]
WorkingDirectory=/pfad/zum/docker-compose-verzeichnis
ExecStart=/usr/local/bin/docker-compose up -d
Restart=always
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
⚠️ Adjust the WorkingDirectory path to the folder containing docker-compose.yml (/home/npm)
Activate and start:
sudo systemctl daemon-reexec
sudo systemctl enable npm-stack
sudo systemctl start npm-stack
✅ Result:
Now the entire NPM stack—including mariadb-aria—starts automatically when the system boots, without having to manually run docker compose up. We start the NPM container by navigating to the /home/npm/ directory and executing the following command:
docker compose up -d
Now we open the NPM admin interface using: http://IP-ADDRESS:81 and get the following screen:

We log in with: – admin@example.com – changeme And we can now assign our own user credentials. It’s important now that we create various subdomains: – npm.domain.de – matrix.domain.de The subdomains point to the VPS on which everything is installed in Hetzner’s DNS settings. Once we’ve done that, we’ll continue with NPM:

In NPM we now click on “Hosts” -> “Proxy Hosts” -> “Add Proxy Host” and add the following:

⚠️ Important: The containers’ internal Docker IPs are used for forwarding. This prevents future errors. We set up the special Docker network at the very beginning of the tutorial for this purpose. All containers are on the same network. Continue: Then click on the SSL tab:

Now click Save and you’ve set up a reverse proxy. Your Matrix server is now accessible via https:// at “matrix.domainname.de.” Do the same with NPM to ensure it’s also secured via SSL:

And again request the SSL certificate under SSL.
The SSL certificates can now be viewed in NPM under “SSL Certificates”:

✅ Done! Matrix Synapse with NPM (both in Docker containers) has been installed on a VPS on the internet.
If you want to know how to install Element Call, check out Patrick’s tutorial:
https://www.cleveradmin.de/blog/2025/04/matrixrtc-element-call-backend-einrichten/

bc1q8dxp9mlt3mkvaklu2vn8j6jteqyv53kschc90v

Lightning: tom@blitz.cmdsrv.de
