Files
website-hadow.fr/_posts/2026-04-30-self-hosting-a-matrix-instance-with-podman.md
T
2026-05-14 14:07:37 +02:00

11 KiB

layout, author, tags
layout author tags
post Sam Hadow messaging podman sysadmin

In this blog post I'll describe how to self host a matrix.org instance using podman. The homeserver used will be synapse.

What are matrix.org and synapse?

First a short introduction:
matrix.org is an open source, secure and decentralized communication protocol. Secure because it defines how end to end encryption should be implemented in the clients.
With matrix.org you have homeservers and clients. Homeservers are what the clients connect to and can federate with each other (meaning someone with an account on homeserver A can talk to someone with an account on homeserver B).
Bridges to other messaging services can also be hosted alongside a homeserver. Hosting your own homeserver allows you to host the bridges you want.
In this blog post I explain how to self host a specific homeserver, which is maintained by the matrix.org foundation: synapse.

self hosting synapse

initial setup

creating secret and directories

Create the database secret not to expose it in your configuration files:

echo -n "<postgres-pass>" > /tmp/secret
podman secret create synapse_postgres_pass /tmp/secret

Adapt the path to your own path for the directories:

mkdir -p /home/data/podman/synapse/{db,config,media,logs}

generating the configuration file

(adapt your-domain to your own, for example: example.org)

podman pod create --name synapse -p 8008:8008 -m=2048m

podman run -it --rm \
    -v /home/data/podman/synapse/config:/data:Z \
    -e SYNAPSE_SERVER_NAME=<your-domain> \
    -e SYNAPSE_REPORT_STATS=no \
    docker.io/matrixdotorg/synapse:latest generate

modifying the configuration file

The previous command will generate a homeserver.yaml file. You'll have to modify this file before using synapse. You'll have to modify at least the following parts:

  • modify the database part (replace the password)
database:
  name: psycopg2
  txn_limit: 10000
  args:
    user: synapse
    password: <POSTGRES_PASSWORD>
    dbname: synapse
    host: 127.0.0.1
    port: 5432
    cp_min: 5
    cp_max: 10
  • set enable_registration to false unless you want users to register freely on your instance.
  • set a shared secret file with registration_shared_secret_path to have access to an API to create users. Be sure to use a secure secret as anyone having this secret can register on your instance as an admin. Also keep in mind the path is relative to the container, not the host.

reverse proxy

In this guide the port used is 8008, we'll use nginx to server the synapse homeserver on the port 443.
(adapt your-domain to your own)
This part is important for users to have the name user@your-domain while hosting synapse on a subdomain. And it's also important for clients and other homeserver to recognize your server.

in sites-available:

# synapse.conf
server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name synapse.<your-domain>;

    ssl_certificate /etc/letsencrypt/live/<your-domain>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<your-domain>/privkey.pem;

    location /.well-known/matrix/server {
      return 200 '{"m.server": "synapse.<your-domain>:443"}';
      add_header Content-Type application/json;
    }
    location /.well-known/matrix/client {
      return 200 '{"m.homeserver": {"base_url": "https://synapse.<your-domain>"},"m.identity_server": {"base_url": "https://vector.im"}}';
      add_header Content-Type application/json;
      add_header "Access-Control-Allow-Origin" *;
    }

    location / {
        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;

        # Nginx by default only allows file uploads up to 1M in size
        # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
        client_max_body_size 256M;

        # Synapse responses may be chunked, which is an HTTP/1.1 feature.
        proxy_http_version 1.1;
    }

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

In your http.conf add this snippet in the https server block:

location /.well-known/matrix/server {
    return 200 '{"m.server": "synapse.<your-domain>:443"}';
    add_header Content-Type application/json;
    add_header "Access-Control-Allow-Origin" *;
}

location /.well-known/matrix/client {
    return 200 '{"m.homeserver": {"base_url": "https://synapse.<your-domain>"},"m.identity_server": {"base_url": "https://vector.im"}}';
    add_header Content-Type application/json;
    add_header "Access-Control-Allow-Origin" *;
}

running the pod

with podman command line

Again adapt the path:
The database version can be different but it needs to be pinned to a specific version to avoid issues with updates. You'll have to manually update this container.

podman run -d --pod=synapse \
    --secret synapse_postgres_pass,type=env,target=POSTGRES_PASSWORD \
    -e POSTGRES_DB="synapse" \
    -e POSTGRES_USER="synapse" \
    -e POSTGRES_INITDB_ARGS="--encoding=UTF-8 --lc-collate=C --lc-ctype=C" \
    -v /home/data/podman/synapse/db:/var/lib/postgresql/data:Z \
    --name=synapse-db \
    docker.io/library/postgres:16

podman run -d --pod=synapse \
    -e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
    -v /home/data/podman/synapse/config:/data:Z \
    -v /home/data/podman/synapse/media:/data/media:z \
    -v /home/data/podman/synapse/logs:/data/logs:Z \
    --name=synapse-app \
    --label io.containers.autoupdate=registry docker.io/matrixdotorg/synapse:latest

Then you can generate the systemd services:

cd ~/.config/systemd/user/
podman generate systemd --restart-policy=on-failure --files --new --name synapse

systemctl --user daemon-reload
systemctl --user enable --now pod-synapse.service

with systemd services directly

Create these files in ~/.config/systemd/user/.

# pod-synapse.service

[Unit]
Description=Podman pod-synapse.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/run/user/1000/containers
Wants=container-synapse-app.service container-synapse-db.service
Before=container-synapse-app.service container-synapse-db.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStartPre=/usr/bin/podman pod create \
	--infra-conmon-pidfile %t/pod-synapse.pid \
	--pod-id-file %t/pod-synapse.pod-id \
	--exit-policy=stop \
	--name synapse \
	-p 8008:8008 \
	-m=2048m \
	--replace
ExecStart=/usr/bin/podman pod start \
	--pod-id-file %t/pod-synapse.pod-id
ExecStop=/usr/bin/podman pod stop \
	--ignore \
	--pod-id-file %t/pod-synapse.pod-id  \
	-t 10
ExecStopPost=/usr/bin/podman pod rm \
	--ignore \
	-f \
	--pod-id-file %t/pod-synapse.pod-id
PIDFile=%t/pod-synapse.pid
Type=forking

[Install]
WantedBy=default.target
# container-synapse-db.service
[Unit]
Description=Podman container-synapse-db.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-synapse.service
After=pod-synapse.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--rm \
	--pod-id-file %t/pod-synapse.pod-id \
	--sdnotify=conmon \
	--replace \
	-d \
	--secret synapse_postgres_pass,type=env,target=POSTGRES_PASSWORD \
	-e POSTGRES_DB=synapse \
	-e POSTGRES_USER=synapse \
	-e "POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C" \
	-v /home/data/podman/synapse/db:/var/lib/postgresql/data:Z \
	--name=synapse-db \
	docker.io/library/postgres:16
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target
# container-synapse-app.service

[Unit]
Description=Podman container-synapse-app.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
BindsTo=pod-synapse.service
After=pod-synapse.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--rm \
	--pod-id-file %t/pod-synapse.pod-id \
	--sdnotify=conmon \
	--replace \
	-d \
	-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
	-v /home/data/podman/synapse/config:/data:Z \
	-v /home/data/podman/synapse/media:/data/media:z \
	-v /home/data/podman/synapse/logs:/data/logs:Z \
	--name=synapse-app \
	--label io.containers.autoupdate=registry docker.io/matrixdotorg/synapse:latest
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

managing the homeserver

registering users

With access to the server, creating the user interactively:

podman exec -it synapse-app /bin/sh
register_new_matrix_user --user <username> --config /data/homeserver.yaml

Or with the registration API, you can use this script.

what about bridges?

Bridges allow you to receive message from other services in matrix. In this section I'll give an example on how to self host a bridge alongside your matrix instance with the discord bridge.

discord bridge example

Creating the initial bridge files:

mkdir -p /home/data/podman/synapse/mautrix-discord
podman run --rm -v /home/data/podman/synapse/mautrix-discord:/data:z dock.mau.dev/mautrix/discord:latest

Create the database: (adapt the password)

podman exec -it synapse-db /bin/bash
psql -U synapse
CREATE USER discordmautrix WITH PASSWORD 'password';
CREATE DATABASE discordmautrix WITH OWNER = 'discordmautrix' ;

Then delete synapse container and recreate it with the bridge registration file: *(don't forget to regenerate, or adapt, the systemd service)

podman run -d --pod=synapse \
-e SYNAPSE_CONFIG_PATH=/data/homeserver.yaml \
-v /home/data/podman/synapse/config:/data:Z \
-v /home/data/podman/synapse/media:/data/media:z \
-v /home/data/podman/synapse/logs:/data/logs:Z \
-v /home/data/podman/synapse/mautrix-discord-registration.yaml:/data/mautrix-discord-registration.yaml:Z \
--name=synapse-app \
--label io.containers.autoupdate=registry docker.io/matrixdotorg/synapse:latest

using the bridge

You'll then have access to a user "discord bridge bot" on your instance where you can log in into your discord account and then bridge your messages.