Unify Signal, WhatsApp and SMS in a personal Matrix server: Part 1 (Matrix)

Goal: Use a single app to satisfy all my messaging needs. Bonus points if I can get it working on a PinePhone

Good overview from https://docs.mau.fi/bridges/go/whatsapp/index.html by http://matrix.to/#/@azata:gazizova.net


  • a reasonably recent version of Docker
  • a reasonably recent version of Docker-Compose
  • somewhere to deploy this and a corresponding domain name (preferable) or IP address
    • See naming considerations here
  • a Matrix client of some sort
    • I have not found any Android clients which work with a Matrix server deployed locally on a LAN
  • an example docker-compose.yaml at the bottom of this post, needs a tiny bit of modification
  • a rudimentary understanding of Matrix and Matrix bridges


Just to be upfront, this is all a bit of a pain in the ass. The configuration is stateful, so there’s no docker-compose up that I’m aware of that will “just work”. The pieces need to be done one at a time.

  1. We’ll configure a Matrix server – Synapse, start it up, and add a user
  2. We’ll start a Signal daemon (signald), configure a Matrix-Signal bridge (mautrix-signal) then integrate it with our Matrix server
  3. We’ll configure a Matrix-WhatsApp bridge, then integrate it with our Matrix server

What’s not included:

  • SSL configuration
  • Matrix user definition/roles (beyond the bare minimum)
  • double puppeting
  • considerations for sharing this server with others

Create volumes for shared persistence

  1. Create Docker volumes that we’ll use throughout the rest of this
    • note these must match the names in the docker-compose.yaml, so either don’t change them or change them both places
    • docker volume create synapse-data
    • docker volume create whatsapp-bridge-data
    • docker volume create signal-bridge-data

Matrix server

  1. make sure you update the docker-compose.yaml to contain an actual password for synapse-db
  2. use an ephemeral Docker container to generate the Synapse configuration file
    1. docker run --rm -e SYNAPSE_SERVER_NAME=nuc.localdomain -e SYNAPSE_REPORT_STATS=yes -v synapse-data:/data matrixdotorg/synapse:latest generate
    2. note that it’s extremely unlikely you’ll have the same Synapse server name, so ensure you’re customizing it. This value should be what you decided in the prerequisites above.
    3. additionally note that the volume must be the same as step 1 above (and the docker-compose.yaml file)
$ docker run --rm -e SYNAPSE_SERVER_NAME=nuc.localdomain -e SYNAPSE_REPORT_STATS=yes -v synapse-data:/data matrixdotorg/synapse:latest generate
/usr/local/lib/python3.8/site-packages/twisted/conch/ssh/common.py:14: CryptographyDeprecationWarning: int_from_bytes is deprecated, use int.from_bytes instead
  from cryptography.utils import int_from_bytes, int_to_bytes
Generating config file /data/homeserver.yaml
Generating signing key file /data/nuc.localdomain.signing.key
A config file has been generated in '/data/homeserver.yaml' for server name 'nuc.localdomain'. Please review this file and customise it to your needs.
  1. now we have a Docker volume (a Docker managed directory) that is populated with the most recent template homeserver.yaml by a Docker container that attached to the volume, generated it, then removed itself after generating. This next step is configuring this homeserver.yaml so you can ultimately run Synapse.
    1. as we’re using a Docker volume, the first step is to actually find the file we want to change:
      • docker inspect --format='{{.Mountpoint}}' synapse-data
      • this should show you something like: /var/lib/docker/volumes/synapse-data/_data
    2. you can look in that directory (probably going to need sudo) or we can go straight to modifying it (use whatever editor you’re comfortable with
      • sudo nano $(docker inspect --format='{{.Mountpoint}}' synapse-data)/homeserver.yaml
    3. Assuming you used the right server_name, there’s only one thing we have to update right now
      • search for database:
      • comment out the existing configuration (sqlite3)
      • uncomment the Postgres configuration (psycopg2)
      • update the user to the synapse-db username in the docker-compose.yaml
      • update the password to the synapse-db password in the docker-compose.yaml
      • update the database to the synapse-db database name in the docker-compose.yaml
      • update the host to synapse-db (the host Docker-Compose introduces)
      • Save and exit, assuming this section of the homeserver.yaml looks like:
  name: psycopg2
    user: synapse     
    password: reallyStrongPassword!1
    database: synapse_db
    host: synapse-db
    cp_min: 5
    cp_max: 10

# For more information on using Synapse with Postgres, see `docs/postgres.md`.
#  name: sqlite3
#  args:
#    database: /data/homeserver.db
  1. get that sweet endorphin hit of progress by starting up Synapse!
    1. docker-compose up synapse
    2. navigate to your host (don’t forget the port!), for me: http://nuc.localdomain:8008
    3. if you have anything but sweet joy, see the troubleshooting section towards the end of this post
      • keep an eye on the logs for anything suspicious
  2. you can ctrl+c to escape
Successful Synapse start up page!
  1. Unfortunately, our Matrix server has no users, so it’s not all that useful. With the container running, lets add one to get this moving
$ docker-compose exec synapse register_new_matrix_user http://nuc.localdomain:8008 -c /data/homeserver.yaml
New user localpart [root]: toby
Confirm password: 
Make admin [no]: yes
Sending registration request...

Done this piece!

Synapse the Matrix server is running, it has a user associated with it. Now is a good time to get used to what it looks like running, maybe try out clients to see which ones can actually connect to your set up, generally troubleshoot things.


  • during set up, the containers are completely stateless – all the state is in the volumes. This means you’re free to delete and recreate them at your leisure
  • generally speaking, if you need to update a configuration file (e.g. homeserver.yaml), restart the relevant service afterwards
  • The “nuclear” option is to stop and remove all containers as well as removing all volumes. This shouldn’t be done lightly, you’ll have to start over. The command (executed in the directory where your docker-compose.yaml is: docker-compose down --volumes to remove everything your docker-compose file manages, then you’ll also have to remove the external named volumes with docker volume rm synapse-data whatsapp-bridge-data signal-bridge-data.

Example docker-compose.yaml

version: "3.7"

  # We need to do some initial set up outside of Docker-Compose, so make these volumes external
    external: true
    external: true
    external: true

  # The Signal daemon (Signald) and the Signal bridge (Mautrix-Signal) need to share a volume

  # Use these named volumes just for convenience

    container_name: synapse-db
    image: postgres:13-alpine
    restart: unless-stopped
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: <GENERATE A PASSWORD HERE> # Make sure to update this!
      POSTGRES_DB: synapse_db
      # ensure the database gets created correctly
      # https://github.com/matrix-org/synapse/blob/master/docs/postgres.md#set-up-database
      POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
      - synapse-db:/var/lib/postgresql/data
      - 5435:5432/tcp

    container_name: synapse
    image: docker.io/matrixdotorg/synapse:latest
    # Since synapse does not retry to connect to the database, restart upon failure
    restart: unless-stopped
      - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
      - synapse-data:/data
      - signal-bridge-data:/signal-bridge
      - whatsapp-bridge-data:/whatsapp-bridge
      - synapse-db
      - 8008:8008/tcp
      - 8448:8448/tcp

    container_name: whatsapp-bridge
    image: dock.mau.dev/tulir/mautrix-whatsapp:latest
    restart: unless-stopped
    - whatsapp-bridge-data:/data
      - 29318:29318/tcp

  # Signal and bridge
    container_name: signald
    image: finn/signald:latest
    restart: unless-stopped
    - signald-data:/signald
    container_name: signal-bridge-db
    image: postgres:13-alpine
    restart: unless-stopped
      POSTGRES_USER: mautrix_signal
      POSTGRES_DB: signal_bridge_db
      POSTGRES_PASSWORD: <GENERATE A PASSWORD HERE> # Make sure to update this!
    - signal-bridge-db:/var/lib/postgresql/data
        - 5434:5432/tcp
    container_name: signal-bridge
    image: dock.mau.dev/tulir/mautrix-signal
    restart: unless-stopped
    - signal-bridge-data:/data
    - signald-data:/signald
      - 29328:29328/tcp
      - signal-bridge-db
      - signald

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s