Beyond Signal: Regaining Control with a Self-Hosted Matrix Server

Recent messenger outages highlight a key weakness. In this guide I demonstrate how to build a selfhosted, resilient chat service.

Beyond Signal: Regaining Control with a Self-Hosted Matrix Server

Recent outages of major online services, such as the one that affected Signal recently, serve as a stark reminder of the fragility inherent in centralized infrastructures. When a single provider like AWS experiences issues, the ripple effects can silence communication platforms used by millions. This situation, however, presents an opportunity to explore more resilient, decentralized alternatives, and this guide focuses on setting up a private Matrix homeserver using Synapse, offering a path to digital sovereignty over personal communications.

audio-thumbnail
Podcast regaining control with a selfhosted matrix server
0:00
/300.130958

Why should I even consider a change?

To answer that question, we have to understand the differences between the two concepts:

Decentralization
  • Matrix is federated: anyone can run a server (like Synapse), and these servers talk to each other. It’s like email — no single company controls it.
  • Signal is centralized: all traffic flows through Signal’s official servers. You can’t self-host or federate.

→ Advantage Matrix: You keep control over data, infrastructure, and governance.

Open Protocol vs. App
  • Matrix is an open protocol — a specification anyone can implement. Synapse is just one implementation.
  • Signal is a closed ecosystem — only the official app and servers exist.

→ Advantage Matrix: extensibility. You can integrate IoT, bots, VR chat, or enterprise systems using the same protocol.

Interoperability
  • Matrix can bridge to other systems (Slack, Discord, IRC, Telegram, etc.) through “bridges.”
  • Signal intentionally isolates itself — no bridges, no federation.

→ Advantage Matrix: great for organizations wanting unified communication or migration paths.

Persistence and Control
  • Matrix allows you to host your own history, enforce your own data retention, and control backups.
  • Signal keeps history only on devices — no server storage, no export options.

→ Advantage Matrix: suitable for teams, archiving, and long-term infrastructure.

Scalability & Customization
  • Synapse can be tuned, sharded, or replaced by other servers (like Dendrite or Conduit).
  • Signal’s infrastructure is opaque — only Signal.org can scale it.

→ Advantage Matrix: more flexibility for deployment and innovation.

Privacy & Security Trade-offs
  • Signal offers simpler, stricter privacy guarantees — centralized servers, audited code, minimal metadata.
  • Matrix uses end-to-end encryption (Olm/Megolm), but encryption is optional per room, and metadata can still leak across federated servers.

→ Advantage Signal: cleaner security model, easier trust.
→ Advantage Matrix: more versatile but requires good configuration.

In short: Matrix is to Signal what Linux is to an iPhone — more complex, but vastly more powerful and open. This might be the reason, why the german military is switching to Matrix.

The weaknesses of the two competitors

Both have their pros ans cons. Here is what I See as a downside:

Signal

  1. Centralization
    Signal’s servers are controlled solely by Signal.org. If they go down or get blocked (as happened in some countries), users are stuck. No federation, no fallback.
  2. Phone Number Identity
    Accounts require a real phone number. That’s a privacy flaw — it links your identity to your communications.
  3. Closed Ecosystem
    No bridges, no bots, no API for external integration. Signal deliberately limits extensibility to preserve simplicity, but that also kills innovation.
  4. Limited Backup and Data Portability
    Messages live only on devices. There’s no cloud sync, multi-device history, or export (beyond encrypted local backups). Lose your phone, lose your chat history.
  5. Dependency on Signal Foundation
    Development is tightly controlled by a small organization. Even though the app and protocol are open-source, direction and infrastructure aren’t community-driven.
  6. Scalability and Federation Trade-off
    It scales fine centrally, but it can’t expand horizontally through user-hosted servers. Growth depends entirely on Signal.org’s resources.

Matrix / Synapse

  1. Complexity and Maintenance
    Running your own Synapse server means managing PostgreSQL, workers, certificates, federation settings, and updates. It’s not plug-and-play.
    → This scares off casual users and small groups. Not the enthusiasts though ;-)
  2. Performance Overhead
    Synapse (the reference server) is written in Python and can become resource-heavy on larger federations. It’s improving, but alternatives like Dendrite or Conduit are still maturing.
  3. Metadata Exposure
    Federation means metadata (who talks to whom, when, and from which domain) can leak between servers. Even with E2E encryption, metadata privacy isn’t airtight.
  4. End-to-End Encryption UX
    Key management and message decryption across multiple devices can be buggy or confusing. Message replays, undecryptable errors, and session resets happen more often than they should.
  5. Fragmentation Risk
    Because it’s a protocol, not a product, implementations vary in maturity and features. Compatibility issues can appear between clients or servers if not kept up to date.
  6. Onboarding Friction
    New users don’t instantly understand homeservers, federation, or room aliases. It feels “technical,” not consumer-friendly.

But lets not get frustrated before we have even started. I wanted to try this and you are invited to join me on my journey, if you like.

The Components of My Private Chat Service

Before diving into the configuration, it’s helpful to understand the key players in this setup. This isn’t just one piece of software, but a small ecosystem working in concert.

  • Matrix: This is the open standard for real-time, decentralized, and encrypted communication. Think of it as the underlying protocol, like SMTP for email, but for instant messaging, VoIP, and more. Its key feature is federation, allowing users on different, independently-hosted servers to communicate with each other seamlessly.
  • Synapse: This is the most mature homeserver implementation for the Matrix protocol, developed by the Matrix.org Foundation. The homeserver is the core piece of the puzzle; it stores user accounts, message histories, and handles communication with other homeserver in the Matrix federation.
  • Element: A popular and feature-rich client for the Matrix network. It’s available for web, desktop, and mobile platforms. For this setup, the element-web client will be deployed, providing a web-based chat interface accessible from any browser.
  • PostgreSQL: While Synapse can run with a simple SQLite database, performance and scalability are significantly better with a more robust database system. PostgreSQL is the recommended choice for any serious Synapse deployment.
  • Docker: To keep things clean, isolated, and easily manageable, the entire stack will be run in containers using Docker and Docker Compose. This simplifies deployment and dependency management immensely.

Prerequisites

To follow this guide, a few things should be in place:

  1. A server (VPS or a machine at home) running a Linux distribution.
  2. Docker and Docker Compose installed on the server.
  3. A domain name (e.g., your-domain.com) and the ability to configure its DNS records. For this guide, the server will be reachable at matrix.your-domain.com.
  4. A reverse proxy to handle incoming traffic and manage SSL/TLS certificates. This is crucial for security. A setup using Nginx-Proxy-Manager is an excellent choice, but any reverse proxy like Traefik or Caddy will work.

Step 1: Generating the Initial Synapse Configuration

The first step is not to write the configuration from scratch, but to let Synapse generate a default homeserver.yaml file for us. This file contains all the possible configuration options, most of which can be left at their defaults.

Create a directory for the project, for example, matrix-server, and navigate into it. Then, run the following Docker command, replacing matrix.your-domain.com with your actual server name:

docker run -it --rm \
    -v $(pwd)/matrix:/data \
    -e SYNAPSE_SERVER_NAME=matrix.your-domain.com \
    -e SYNAPSE_REPORT_STATS=no \
    matrixdotorg/synapse:latest generate

This command will:

  • Start a temporary Synapse container.
  • Generate a configuration file tailored to the specified SYNAPSE_SERVER_NAME.
  • Place the generated homeserver.yaml and a signing key into a new directory named matrix.
  • Disable anonymous statistics reporting.

After running the command, a matrix directory will be present, containing the precious homeserver.yaml.

Step 2: The Heart of the Setup - docker-compose.yml

Now it’s time to define the services using Docker Compose. Add the following to the docker-compose.yml file and populate it with the following content. This file defines the Synapse server, its PostgreSQL database, and the Element Web client.

  matrix:
    image: matrixdotorg/synapse:latest
    container_name: matrix
    restart: unless-stopped
    volumes:
      # Use the generated configuration and let synapse store its media here
      - ./matrix:/data
    environment:
      # These must match the values used during generation
      - SYNAPSE_SERVER_NAME=matrix.your-domain.com
      - SYNAPSE_REPORT_STATS=no
      - TZ=Europe/Berlin
    expose:
      # Expose port 8008 to the NPM
      - "8008"
    depends_on:
      - matrixdb
    labels:
      # Optional: Enable automatic updates with Watchtower
      - "com.centurylinklabs.watchtower.enable=true"
    networks:
      - web
      - backend

  matrixdb:
    image: postgres:15-alpine
    container_name: matrixdb
    restart: unless-stopped
    environment:
      - POSTGRES_USER=synapse
      - POSTGRES_PASSWORD=YOUR_SECURE_POSTGRES_PASSWORD
      - POSTGRES_DB=synapse
      # Required settings for Matrix Synapse
      - POSTGRES_INITDB_ARGS=--encoding='UTF8' --lc-collate='C' --lc-ctype='C'
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - backend

  element-web:
    image: vectorim/element-web:latest
    container_name: element-web
    restart: unless-stopped
    expose:
      # Expose port 80 only to other containers (like the reverse proxy)
      - "80"
    volumes:
      - ./element-config/config.json:/app/config.json:ro
    environment:
      - TZ=Europe/Berlin
    networks:
      - web

networks:
  web:
    # This network should be shared with your reverse proxy
    external: true
  backend:
    # This network is internal to our application stack
    internal: true

Important Notes:

  • Replace YOUR_SECURE_POSTGRES_PASSWORD with a strong, unique password.
  • The web network is marked as external. This assumes a pre-existing Docker network that is shared with the reverse proxy. This is the standard way to connect application containers to a proxy without exposing ports on the host machine.
  • The backend network is internal and ensures the Synapse container can reach the database, but nothing else can.

Step 3: Customizing the Synapse Homeserver

The default homeserver.yaml is configured to use SQLite. It must be modified to connect to the PostgreSQL database defined in the docker-compose.yml.

Open the file matrix/homeserver.yaml and locate the database: section. Replace the entire block with the following, making sure to use the same credentials as in the docker-compose.yml:

# homeserver.yaml

# ... other settings ...

database:
  name: psycopg2
  args:
    user: synapse
    password: "YOUR_SECURE_POSTGRES_PASSWORD"
    database: synapse
    host: matrixdb  # This is the service name of the database container
    cp_min: 5
    cp_max: 10

# ... other settings ...

While in this file, it’s also a good idea to disable public registration to keep the server private. Find the line enable_registration and ensure it is set to false:

# homeserver.yaml

# ...
# Enable registration for new users.
#
enable_registration: false
# ...

The various secret keys (macaroon_secret_key, form_secret, etc.) in this file were generated automatically in the first step and should be kept confidential. There is no need to change them.

Step 4: Pointing Element to Our Server

The Element Web client needs to know which homeserver to connect to by default. Create a new directory element-config and inside it, a file named config.json.

# element-config/config.json
{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.your-domain.com"
    },
    "m.identity_server": {
      "base_url": "https://vector.im"
    }
  },
  "disable_custom_urls": true,
  "disable_guests": true
}

This configuration tells Element to connect to https://matrix.your-domain.com by default and disables some features like guest access for a more private setup.

Step 5: Launch and First User Creation

With all the configuration files in place, the entire stack can be brought online with a single command:

docker compose up -d

Docker will now pull the necessary images and start the containers. After a minute or two, the services should be running.

Since public registration was disabled, the first user must be created manually via the command line. This user will automatically be granted server administrator privileges.

# Replace 'matrix' if you used a different container_name
docker exec -it matrix register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008

You will be asked for a username, password and permissions for this user. If successful, the command will output a confirmation, and the first user is ready to log in.

Step 6: Exposing the Services to the World (Securely)

The final step is to configure the reverse proxy. The proxy needs to direct traffic for matrix.your-domain.com to the Synapse container and traffic for the web client (e.g., element.your-domain.com) to the Element container.

Below is a conceptual Nginx configuration. The exact implementation will depend on the reverse proxy being used. If you used my approach, it should look like this:

I encountered some issues while Block Common Exploits was enabled, so I turned it off. My SIEM is already monitoring my systems and auto-responds with firewall block rules, once malicious traffic ist detected.

Under Advanced, the following has to be entered:

location ~ ^(/_matrix|/_synapse/client) {
    proxy_pass http://matrix:8008;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
    # Adjust these for large client/server interactions
    client_max_body_size 50M; # Beispielwert, je nach Anforderung anpassen
    proxy_read_timeout 300;
    proxy_send_timeout 300;
    proxy_buffering off;
    proxy_request_buffering off; # Für Streaming-APIs
    
    # WebSocket Support
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

# .well-known URI für Client-Server Discovery
# Dies teilt Clients mit, wo der Homeserver ist.
location /.well-known/matrix/client {
    default_type application/json;
    return 200 '{"m.homeserver": {"base_url": "https://matrix.your-domain.com/"}}';
}

# .well-known URI für Server-Server Discovery (Federation)
# Dies teilt anderen Matrix-Servern mit, dass sie sich über Port 443 verbinden sollen.
location /.well-known/matrix/server {
    default_type application/json;
    return 200 '{"m.server": "matrix.your-domain.com:443"}';
}

This allows the federation to discover the server and its users.

For Element-Web to work, you have to create an additional entry on the NPM. I chose chat.your-domain.com for the element-web-Container:

Once the reverse proxy is configured and reloaded, it should be possible to navigate to https://chat.your-domain.com in a browser and log in with the credentials created in the previous step.

Testing the Setup

If everything was successful, a private Matrix server and a web frontend are now ready. The first test is to install Element on a smartphone. During setup, instead of accepting the default server, specify the custom server address (matrix.your-domain.com) and log in. After creating a second user on the server, a test chat can be initiated.

Smartphone

I installed Element-X on my phone and when asked, I entered my own domain and credentials. Since I created two users on my server, I can invite the other for a test-chat:

OK. Lets have a chat.

Perfect. Everything seems alright on the Phone. Now lets see, if the Browser can access the Matrix, too.

In the Browser

Once entered the URL, you have to login obviously. After that, you should go through the settings, to adjust everything to your liking:

Now lets see, if everything works as expected:

I will say: It was surprisingly straightforward.

Conclusion

By leveraging open-source tools like Matrix, Synapse, and Docker, it is possible to build a private, secure, and resilient communication platform. This setup not only grants full control over personal data but also builds immunity against outages of large, centralized cloud providers. Taking the time to self-host critical services is a powerful step towards achieving true digital independence.