Your own homeserver
This page sets up a Matrix homeserver with bridges to Messenger, Instagram, Slack and Discord, then connects this app to it. It is the recipe the developer runs at home: one Linux machine with Docker, reachable over the home network and Tailscale, with no federation.
Allow an afternoon. You need:
- A Linux machine that stays on, with Docker and Docker Compose.
- A domain you control, for the homeserver's address and its HTTPS certificate.
- A reverse proxy that serves HTTPS. The examples use Caddy.
WhatsApp, Signal & friends explains what a bridge can see before you decide to run one.
Pick a server name
Every Matrix ID ends with the server name: @you:example.org. It is written
into every user, room and signing key, and it cannot be changed later without
starting again from an empty server.
The server name does not have to be the machine's address. Here the server
name is example.org and the homeserver answers at matrix.example.org. With
federation off, nothing else is needed to make that work, because this app
asks for the homeserver's address directly.
The compose file
One Postgres holds a database for Synapse and one per bridge. Put this in an
empty directory as docker-compose.yml, with a .env next to it that sets
POSTGRES_PASSWORD to a long random string.
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: synapse
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: synapse
POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- ./postgres:/var/lib/postgresql/data
configs:
- source: bridge-databases
target: /docker-entrypoint-initdb.d/bridges.sql
synapse:
image: matrixdotorg/synapse:v1.161.0
restart: unless-stopped
depends_on: [postgres]
volumes:
- ./synapse:/data
mautrix-meta:
image: dock.mau.dev/mautrix/meta:v0.2609.0
restart: unless-stopped
volumes:
- ./bridges/meta:/data
mautrix-instagram:
image: dock.mau.dev/mautrix/meta:ig-v0.2609.0
restart: unless-stopped
volumes:
- ./bridges/instagram:/data
mautrix-slack:
image: dock.mau.dev/mautrix/slack:v0.2609.0
restart: unless-stopped
volumes:
- ./bridges/slack:/data
mautrix-discord:
image: dock.mau.dev/mautrix/discord:v0.7.7
restart: unless-stopped
volumes:
- ./bridges/discord:/data
configs:
bridge-databases:
content: |
CREATE DATABASE meta OWNER synapse;
CREATE DATABASE instagram OWNER synapse;
CREATE DATABASE slack OWNER synapse;
CREATE DATABASE discord OWNER synapse;Messenger and Instagram are two separate bridges from the same project, with two images and two databases. Pin whatever versions are current when you set up; the ones above were current in September 2026.
The C collation on Postgres is what Synapse requires. The extra databases are
created only the first time Postgres starts on an empty directory, so add any
later bridge's database by hand with createdb.
Configure Synapse
Generate the configuration and the server's signing key:
docker run --rm -v ./synapse:/data \
-e SYNAPSE_SERVER_NAME=example.org -e SYNAPSE_REPORT_STATS=no \
matrixdotorg/synapse:v1.161.0 generateThen change these fields in synapse/homeserver.yaml:
public_baseurl: https://matrix.example.org/
listeners:
- port: 8008
type: http
x_forwarded: true
bind_addresses: ['0.0.0.0']
resources:
- names: [client]
database:
name: psycopg2
args:
user: synapse
password: <POSTGRES_PASSWORD>
database: synapse
host: postgres
federation_domain_whitelist: []
trusted_key_servers: []
enable_registration: false
app_service_config_files:
- /data/appservices/doublepuppet.yaml
- /data/appservices/meta.yaml
- /data/appservices/instagram.yaml
- /data/appservices/slack.yaml
- /data/appservices/discord.yamlThe listener serves only the client API and the federation list is empty, so the server talks to nobody but its own users and bridges. Synapse 1.114 or newer supports the sliding sync this app needs with no extra setting.
Configure each bridge
Each bridge writes its own configuration the first time it runs, then exits:
docker compose run --rm mautrix-metaEdit bridges/meta/config.yaml:
| Field | Value |
|---|---|
homeserver.address | http://synapse:8008 |
homeserver.domain | example.org |
appservice.address | http://mautrix-meta:29319 |
appservice.hostname | 0.0.0.0 |
database.type | postgres |
database.uri | postgres://synapse:<POSTGRES_PASSWORD>@postgres/meta?sslmode=disable |
bridge.permissions | example.org: user and "@you:example.org": admin |
encryption.allow and encryption.default | true |
Run the same command again. This time it writes registration.yaml, which
tells Synapse how to reach the bridge. Copy it to
synapse/appservices/meta.yaml.
Repeat for the other three. The ports differ: Instagram listens on 29330,
Slack on 29335 and Discord on 29334. Discord's configuration uses an older
layout, so its database fields are under appservice.database and its
encryption fields under bridge.encryption. A field set in the wrong place is
ignored without an error.
With encryption on, chats between this app and the bridge are end-to-end encrypted. The bridge itself decrypts them, on your machine.
Double puppeting
Without double puppeting, a message you send from the real Messenger or Slack app shows up in Matrix as a ghost user with your name. With it, the bridge posts it as you.
Create synapse/appservices/doublepuppet.yaml, with two random strings of
your own:
id: doublepuppet
url:
as_token: <random string>
hs_token: <another random string>
sender_localpart: <a third random string>
rate_limited: false
namespaces:
users:
- regex: '@.*:example\.org'
exclusive: falseThen give each bridge the same as_token. In Messenger, Instagram and Slack
that is double_puppet.secrets; in Discord it is
bridge.login_shared_secret_map:
example.org: as_token:<the as_token above>Put it behind HTTPS
The app needs the client API over HTTPS, plus each bridge's login API so it can sign you in to a network without chat commands. With Caddy:
matrix.example.org {
handle_path /_matrix/provision/facebook/* {
rewrite * /_matrix/provision{uri}
reverse_proxy mautrix-meta:29319
}
handle_path /_matrix/provision/instagram/* {
rewrite * /_matrix/provision{uri}
reverse_proxy mautrix-instagram:29330
}
handle_path /_matrix/provision/slack/* {
rewrite * /_matrix/provision{uri}
reverse_proxy mautrix-slack:29335
}
reverse_proxy /_matrix/* synapse:8008
reverse_proxy /_synapse/client/* synapse:8008
}The app looks for a bridge's login API at /_matrix/provision/ followed by the
bot's name without bot, so @slackbot is at /_matrix/provision/slack/. Each
request carries your own Matrix access token, which the bridge checks with
Synapse. Discord's bridge has no such API, so it is left out.
Caddy has to reach Synapse and those three bridges, so put them on a shared
Docker network. If the machine is only on your home network and Tailscale, a
local DNS entry for matrix.example.org and a DNS-01 certificate are enough;
nothing has to be open to the internet. The bridges only make outgoing
connections.
Start it
docker compose up -d
docker compose exec synapse register_new_matrix_user \
-c /data/homeserver.yaml -u you --admin http://localhost:8008https://matrix.example.org/_matrix/client/versions should answer with a list
of versions. Each bridge's log should end with Bridge started.
In this app, open Settings → Protocols → Matrix and enter
https://matrix.example.org and @you:example.org. Protocols
has the rest.
Sign in to each network
Open Settings → Protocols → Matrix. Under Bridges on this server the app lists every bridge it finds on your homeserver, and who you are signed in as on each. Connect asks the bridge how it can sign you in and walks you through it:
- A form, such as an email and a code, appears as fields in the app.
- A QR code appears on screen, and the app moves on once you have scanned it.
- A website sign-in opens the site in a window of its own. Sign in there as you would in a browser, including two-factor prompts; the window closes by itself once the bridge has what it needs. The app reads only the cookies or values the bridge asks for, and the window keeps nothing afterwards.
On the phone the website opens in a sheet instead of a window.
The website ways are the most reliable for Messenger, Instagram and Slack, because they are the site's own sign-in. Messenger and Instagram sometimes ask you to confirm the new sign-in from their phone apps.
When a bridge has no login API the app can reach, as with Discord, Connect opens a chat with its bot instead and sends the first command. Answer the bot's questions there:
| Bot | Network | Command |
|---|---|---|
@discordbot:example.org | Discord | login-qr, then scan the QR code with the Discord app on your phone |
| Any other bridge bot | login lists the ways it can sign in; send login followed by the one you want |
Once signed in, your chats appear in this app's list as Matrix chats and fill in as the bridge catches up.
Keep it running
- Back up the Postgres databases, all five of them, and the
synapsedirectory. The directory holds the signing key and uploaded media, and the databases hold every bridge's sign-in sessions. - Update by changing the image tags and running
docker compose up -d. Read a bridge's release notes first; a new version sometimes needs a configuration change. - If a network signs the bridge out, press Connect again. Your chats stay where they were.