Version: 25.11.01
In dem folgenden Tutorial wollen wir Matrix Synapse inklusive SSL Zertifikate von Letsencrypt mit dem Nginx Proxy Manager auf einem VPS im Internet (in unserem Fall bei Hetzner.com) installieren.
Ein großes Dankeschön geht an Patrick von https://www.cleveradmin.de/ mit dem ich zusammen eine tolle Lösung erarbeitet habe!!
🔧 Voraussetzungen:
– VPS ist eingerichtet mit root Zugang (Ubuntu)
– Eine Domain ist vorhanden mit Zugriff auf die DNS Einträge
Hier reicht ein kleiner Server von z.B. Hetzner.com aus:

⚠️ Firewall: Bitte die Firewall bei Hetzner konfigurieren und nur folgende Ports zulassen:

TCP: 22, 80, 81, 443 (Matrix Synapse, NPM)
TCP: 7881-7882 und UDP 50100 – 50200 sind die Vorbereitungen für Element Call
⚠️ Der DNS Zugriff muss ebenfalls gegeben sein, bei einer vorhandenen Domain:

Wir starten und loggen uns auf dem Server ein:
ssh root@SERVER-IP
Zuerst sorgen wir dafür das der Server auf dem neusten Stand ist mit folgendem Befehl:
apt-get update && apt-get upgrade && apt-get autoremove
Danach starten wir den Server neu und loggen uns dann erneut ein:
sudo reboot now
Wir erstellen die Ordner für unsere beiden Projekte (Synapse und Nginx Proxy Manager -NPM-)
mkdir -p /home/npm
mkdir -p /home/synapse/data
Jetzt installieren wir erst einmal Docker indem wir im /home/ Verzeichnis eine Datei Namens „install.sh“ erstellen:
nano /home/install.sh
In die Datei kommt folgender Inhalt rein:
#!/usr/bin/env bash
# Script: docker-install-commander.sh
# Purpose: Install Docker & Docker Compose for Ubuntu/Debian
# Author: Tom Commander (IT-Service-Commander.de)
# Date: 2025-11-01
set -euo pipefail
# ===== Settings (optional) =====
# Additionally install the standalone docker-compose binary (besides the plugin)?
INSTALL_COMPOSE_STANDALONE=false
COMPOSE_VERSION="v2.30.3" # only relevant if the above is true
# ===== Helper functions =====
need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Error: '$1' is required."; exit 1; }; }
as_root() { if [ "$(id -u)" -eq 0 ]; then bash -c "$*"; else sudo bash -c "$*"; fi; }
# ===== Detect system =====
need_cmd awk
if [ -r /etc/os-release ]; then
. /etc/os-release
else
echo "Error: /etc/os-release not found. Aborting."
exit 1
fi
ID_LOWER="${ID,,}" # ubuntu | debian | linuxmint | ...
ARCH="$(dpkg --print-architecture)"
# Normalize to an upstream family + codename for Docker repo selection
FAMILY="" # "ubuntu" or "debian"
CODENAME="" # e.g., jammy/noble/bookworm
case "$ID_LOWER" in
ubuntu)
FAMILY="ubuntu"
CODENAME="${VERSION_CODENAME:-}"
;;
debian)
FAMILY="debian"
CODENAME="${VERSION_CODENAME:-}"
;;
linuxmint)
# Regular Linux Mint is Ubuntu-based -> use UBUNTU_CODENAME.
# LMDE is Debian-based -> has DEBIAN_CODENAME.
if [ -n "${UBUNTU_CODENAME:-}" ]; then
FAMILY="ubuntu"
CODENAME="$UBUNTU_CODENAME"
elif [ -n "${DEBIAN_CODENAME:-}" ]; then
FAMILY="debian"
CODENAME="$DEBIAN_CODENAME"
else
echo "Linux Mint detected but could not determine upstream codename (UBUNTU_CODENAME/DEBIAN_CODENAME missing)."
exit 1
fi
;;
*)
echo "This script supports Ubuntu, Debian, and Linux Mint. Detected: ${ID_LOWER}"
exit 1
;;
esac
if [[ -z "$CODENAME" ]]; then
echo "Could not determine upstream codename. Please set VERSION_CODENAME/UBUNTU_CODENAME/DEBIAN_CODENAME."
exit 1
fi
echo "Detected system: ${PRETTY_NAME:-$ID_LOWER} (family: $FAMILY, codename: $CODENAME, arch: $ARCH)"
# ===== Preparation =====
echo "[1/6] Updating package list and installing prerequisites..."
as_root "apt-get update -y"
as_root "apt-get install -y ca-certificates curl gnupg lsb-release"
# Ensure keyring directory exists
as_root "install -m 0755 -d /etc/apt/keyrings"
# ===== Docker GPG key =====
echo "[2/6] Installing Docker GPG key..."
KEY_DST="/etc/apt/keyrings/docker.gpg"
if [[ "$FAMILY" == "ubuntu" ]]; then
as_root "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o '$KEY_DST'"
else
as_root "curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o '$KEY_DST'"
fi
as_root "chmod a+r '$KEY_DST'"
# ===== Add repository (idempotent) =====
echo "[3/6] Adding Docker APT repository..."
DOCKER_LIST="/etc/apt/sources.list.d/docker.list"
REPO_LINE="deb [arch=${ARCH} signed-by=${KEY_DST}] https://download.docker.com/linux/${FAMILY} ${CODENAME} stable"
if [ -f "$DOCKER_LIST" ] && grep -q "download.docker.com/linux" "$DOCKER_LIST"; then
echo "Docker repository already exists: $DOCKER_LIST"
else
as_root "echo '$REPO_LINE' > '$DOCKER_LIST'"
fi
# ===== Install Docker & Compose plugin =====
echo "[4/6] Installing Docker and Compose plugin..."
as_root "apt-get update -y"
as_root "apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"
# ===== Enable and start Docker =====
echo "[5/6] Enabling and starting Docker service..."
as_root "systemctl enable docker >/dev/null 2>&1 || true"
as_root "systemctl restart docker"
# ===== Add user to docker group =====
if [ "$(id -u)" -ne 0 ]; then
echo "[6/6] Adding user '$USER' to the 'docker' group..."
as_root "usermod -aG docker '$USER'"
echo "Note: Log out and back in (or run 'newgrp docker') for group changes to take effect."
else
echo "[6/6] Running as root: skipping user group modification."
fi
# ===== Optional: Install standalone docker-compose binary =====
if [ "$INSTALL_COMPOSE_STANDALONE" = true ]; then
echo "[Optional] Installing standalone docker-compose ${COMPOSE_VERSION}..."
need_cmd uname
as_root "curl -L \"https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose"
as_root "chmod +x /usr/local/bin/docker-compose"
echo "docker-compose version: $(/usr/local/bin/docker-compose --version || true)"
fi
# ===== Show versions =====
echo
echo "✅ Installation complete!"
echo "Docker: $(docker --version 2>/dev/null || echo 'Not in PATH? Try logging out and back in.')"
echo "Docker Compose: $(docker compose version 2>/dev/null || echo 'Plugin not found')"
echo
echo "Tip: Test your setup with -> docker run --rm hello-world"
Anschließend führen wir die Datei aus:
cd /home/
bash install.sh
Nach Abschluss des Skripts ist Docker und Docker-Compose installiert.
⚠️ Wichtig: Jetzt legen wir ein spezielles Netzwerk an, in welchem die einzelnen Container später gemeinsam tätig sind:
docker network create --subnet=172.37.51.0/24 matrixnetwork
Wir fangen mit Matrix Synapse an und gehen dazu in den synapse Ordner und führen folgenden Befehl aus:
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“ wird natürlich vorher mit der eigenen Domain abgeändert. z.b: matrix.deinedomain.de
Das Skript hat jetzt die sogenannte homeserver.yaml Datei im Ordner /synapse/data/ angelegt.
Diese öffnen wir mit:
nano /home/synapse/data/homeserver.yaml
Die Datei sollte wie folgt aussehen:
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
# Alle Medien löschen die älter als 120 Tage sind, spart Speicherplatz auf dem Server !!
media_retention:
local_media_lifetime: 120d
remote_media_lifetime: 120d
#Upload Größe von Dateien auf max. 250 MB setzen
max_upload_size: 250M
# Aktiviert die Bereitstellung der .well-known URL zur Weiterleitung föderativer Kommunikation an Port 443 - wird später für Element Call benötigt !!
#serve_server_wellknown: true
# vim:ft=yaml
⚠️ Wichtig ist die Datenbankeinstellungen noch wie folgt abzuändern:
#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
Dabei wird die SQL3 Datenbank deaktiviert und Postgres aktiviert.
Wir speichern die Datei (STRG+O) und schließen den Editor wieder (STRG+X) und legen nun die „docker-compose.yml“ Datei im Ordner Synapse an:
nano /home/synapse/docker-compose.yml
Dabei könnt ihr die folgende Compose-Datei verwenden und ändert diese mit euren Daten ab (Passwort für Postgres noch abändern):
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
Nun schauen wir, dass wir wieder in das Projektverzeichnis gehen und den Container starten:
cd /home/synapse
docker compose up -d
Das Ergebnis können wir mit „docker ps“ in der Kommandozeile abfragen. Wir sollten sehen, dass die Container (Synapse und die Datenbank) als „Healthy“ läuft:

Den ersten Benutzer (Admin) legen wir mit folgendem Befehl an:
⚠️ Wichtig: wieder in das /home/synapse/ Verzeichnis wechseln!
docker compose exec synapse register_new_matrix_user -u benutzername -p passwort -a -k "geheimes-passwort" http://localhost:8008
Erklärung:
-u: gewünschter Benutzername-p: Passwort-a: macht den Benutzer zum Admin (optional)-k: shared secret aushomeserver.yamlhttp://localhost:8008: Synapse Admin-API-Endpunkt (achte auf korrekte Adresse, evtl.http://synapse:8008innerhalb von Compose)
✅ Fertig: Teil 1 ist erledigt. Matrix Synapse ist installiert. Herzlichen Glückwunsch ! Überprüft werden kann dies mit aufruf der URL:
http://server-ip:8008

Nächster Schritt: NPM – Nginx Proxy Manager als Docker installieren (SSL Zertifikat / HTTPS)
nano /home/npm/docker-compose.yml
In die Datei fügen wir folgenden Code ein:
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
Nun sorgen wir dafür, dass neben dem NPM auch immer die Datenbank beim Systemstart mitgestartet wird. In der docker-compose.yml klappt das nämlich nicht immer und man kann sich dann nicht bei NPM einloggen auf dem Interface.
🔧 Die Lösung ist ein: „systemd-Service“
Service-Datei erstellen (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
⚠️ Passe den Pfad bei WorkingDirectory an den Ordner an, in dem die docker-compose.yml liegt! (/home/npm)
Aktivieren und starten:
sudo systemctl daemon-reexec
sudo systemctl enable npm-stack
sudo systemctl start npm-stack
✅ Ergebnis
Jetzt wird der komplette NPM Stack – inklusive mariadb-aria – beim Booten des Systems automatisch gestartet, ohne dass manuell docker compose up ausgeführt werden muss.
Wir starten den NPM container indem wir in das Verzeichnis /home/npm/ gehen und dort den Befehl ausführen:
docker compose up -d
Nun öffnen wir die Admin-Oberflöche von NPM mittels: http://IP-ADRESSE:81 und bekommen folgendes Bild:

Wir loggen uns ein mit:
– admin@example.com
– changeme
Und können nun unsere eigenen Userdaten vergeben.
Wichtig ist jetzt, dass wir verschiedene Subdomains anlegen:
– npm.domain.de
– matrix.domain.de
Die Subdomains verweisen dabei in den DNS Einstellungen von Hetzner auf den VPS auf dem das alles installiert ist. Nachdem wir das gemacht haben, geht es beim NPM weiter:

Im NPM klicken wir jetzt auf „Hosts“ -> „Proxy Hosts“ -> „Add Proxy Host“ und fügen folgendes ein:

⚠️ Wichtig: Es werden zur Weiterleitung die internen Docker-IPs der Container verwendet. Das verhindert spätere Fehler. Dafür haben wir auch ganz am Anfang des Tutorials das spezielle Docker-Netzwerk eingerichtet. Alle Container sind im gleichen Netzwerk.
Weiter gehts: Dann auf den Reiter SSL klicken:

Nun auf Save klicken und ihr habt einen „Reverse Proxy“ eingerichtet. Eure Matrix Server ist nun per https:// erreichbar unter „matrix.domainname.de“.
Dasselbe macht ihr noch mit dem NPM, damit auch dieser über SSL abgesichert ist:

Und auch wieder unter SSL das SSL Zertifikat anfordern.
Im NPM unter „SSL Certificates“ können nun die SSL Zertifikate eingesehen werden:

✅ Fertig! Matrix Synapse mit NPM (beides im Docker Container) wurde auf einem VPS im Internet installiert.
Ein Tutorial für ElementCall gibt es hier:
https://it-service-commander.de/support-2/tutorials/docker/element-call-als-add-on-fuer-matrix-synapse-docker/
Alternativ:
Tutorial von Patrick
https://www.cleveradmin.de/blog/2025/04/matrixrtc-element-call-backend-einrichten/
![]() | ![]() | |
| bc1pnks6qsyumceygnw760x6dthqzngm3pt5xtpkrh3n9ydm3e8ekgms7r4azl | Lightning: itsc@strike.me | https://paypal.me/TomC777 |
![]() | ![]() | ![]() |





