How to Self-Host Matrix Synapse with Docker Compose

What Is Matrix Synapse?

Matrix is an open, decentralized communication protocol for real-time messaging, voice, and video. Synapse is the reference homeserver implementation — the software you run to participate in the Matrix network. Think of it like email: you run your own server, but you can communicate with anyone on any other Matrix server worldwide.

Synapse replaces Slack, Discord, Microsoft Teams, and other centralized chat platforms. Pair it with Element (the most popular Matrix client) and you get end-to-end encrypted messaging, voice/video calls, file sharing, and bridging to other platforms — all under your control. Federation means your users can chat with anyone on the Matrix network, not just people on your server.

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • A domain name pointed at your server (e.g., matrix.example.com)
  • 2 GB of RAM minimum (4 GB+ recommended for more than a handful of users)
  • 20 GB of free disk space (media uploads grow over time)
  • Ports 8448 (federation) and 443 (reverse proxy) accessible from the internet if you want federation

Docker Compose Configuration

Create a project directory:

mkdir -p /opt/matrix-synapse && cd /opt/matrix-synapse

Create a .env file with your configuration values:

# .env
# REQUIRED: Change all of these before starting

# Your Matrix server name — this is permanent and cannot be changed later.
# Use your base domain (example.com), not a subdomain.
# Users will be @user:example.com
SYNAPSE_SERVER_NAME=example.com

# Domain where Synapse is actually reachable (can differ from server name)
SYNAPSE_SERVER_HOSTNAME=matrix.example.com

# PostgreSQL credentials
POSTGRES_DB=synapse
POSTGRES_USER=synapse
POSTGRES_PASSWORD=change-this-to-a-strong-password

# Synapse secrets — generate with: openssl rand -hex 32
SYNAPSE_REGISTRATION_SHARED_SECRET=generate-a-long-random-string-here
SYNAPSE_MACAROON_SECRET_KEY=generate-another-long-random-string-here
SYNAPSE_FORM_SECRET=generate-yet-another-long-random-string-here

Generate your secrets now:

echo "SYNAPSE_REGISTRATION_SHARED_SECRET=$(openssl rand -hex 32)" >> .env
echo "SYNAPSE_MACAROON_SECRET_KEY=$(openssl rand -hex 32)" >> .env
echo "SYNAPSE_FORM_SECRET=$(openssl rand -hex 32)" >> .env

Create a docker-compose.yml file:

services:
  synapse:
    image: matrixdotorg/synapse:v1.147.1
    container_name: synapse
    restart: unless-stopped
    environment:
      - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml
    volumes:
      - synapse_data:/data
    ports:
      # Client API — expose to reverse proxy only
      - "127.0.0.1:8008:8008"
      # Federation — expose publicly if you want inter-server communication
      - "8448:8448"
    depends_on:
      synapse_db:
        condition: service_healthy
    networks:
      - matrix
    healthcheck:
      test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  synapse_db:
    image: postgres:15-alpine
    container_name: synapse_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      # Required: Synapse needs UTF-8 encoding with C locale
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
    volumes:
      - synapse_db_data:/var/lib/postgresql/data
    networks:
      - matrix
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  synapse_data:
    driver: local
  synapse_db_data:
    driver: local

networks:
  matrix:
    driver: bridge

Generate the Synapse Configuration

Before starting Synapse for the first time, you need to generate the homeserver.yaml configuration file. Run this command (replace example.com with your actual server name):

docker compose run --rm -e SYNAPSE_SERVER_NAME=example.com -e SYNAPSE_REPORT_STATS=no synapse generate

This creates homeserver.yaml inside the synapse_data volume. You need to edit it to use PostgreSQL instead of the default SQLite.

Configure PostgreSQL

Find the generated config file. With named volumes, copy it out, edit it, and copy it back:

# Copy the config out of the volume
docker compose cp synapse:/data/homeserver.yaml ./homeserver.yaml

Open homeserver.yaml and find the database section. Replace the entire database block with:

database:
  name: psycopg2
  args:
    user: synapse
    password: change-this-to-a-strong-password
    database: synapse
    host: synapse_db
    port: 5432
    cp_min: 5
    cp_max: 10

Use the same POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB values from your .env file. The host is synapse_db — the container name on the Docker network.

While you have the file open, also verify or set these values:

server_name: "example.com"
public_baseurl: "https://matrix.example.com/"

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    resources:
      - names: [client, federation]
        compress: false

registration_shared_secret: "your-generated-secret-here"
macaroon_secret_key: "your-generated-secret-here"
form_secret: "your-generated-secret-here"

enable_registration: false

Copy the edited config back into the volume:

docker compose cp ./homeserver.yaml synapse:/data/homeserver.yaml

Ensure correct ownership (Synapse runs as UID 991:GID 991 by default):

docker compose run --rm synapse chown 991:991 /data/homeserver.yaml

Start the Stack

docker compose up -d

Check that both containers are healthy:

docker compose ps

Verify Synapse is responding:

curl http://localhost:8008/_matrix/client/versions

You should get a JSON response listing supported Matrix API versions.

Initial Setup

Create an Admin User

With registration disabled (the safe default), create your first admin user via the command line:

docker compose exec synapse register_new_matrix_user -u admin -p your-secure-password -a -c /data/homeserver.yaml http://localhost:8008

Flags:

  • -u admin — the username (will be @admin:example.com)
  • -p your-secure-password — the password (change this)
  • -a — makes this user a server admin
  • -c /data/homeserver.yaml — path to the config inside the container

You can now log in with any Matrix client using your server URL (https://matrix.example.com) and the credentials you just created.

Element Web Client Setup (Optional)

Element Web is the most popular Matrix client. You can self-host it alongside Synapse for a complete setup.

Add this service to your docker-compose.yml:

  element:
    image: vectorim/element-web:v1.11.96
    container_name: element
    restart: unless-stopped
    volumes:
      - ./element-config.json:/app/config.json:ro
    ports:
      - "127.0.0.1:8080:80"
    networks:
      - matrix

Create element-config.json:

{
    "default_server_config": {
        "m.homeserver": {
            "base_url": "https://matrix.example.com",
            "server_name": "example.com"
        },
        "m.identity_server": {
            "base_url": "https://vector.im"
        }
    },
    "brand": "Element",
    "integrations_ui_url": "https://scalar.vector.im/",
    "integrations_rest_url": "https://scalar.vector.im/api",
    "bug_report_endpoint_url": "https://element.io/bugreports/submit",
    "showLabsSettings": true,
    "default_theme": "dark"
}

Replace matrix.example.com and example.com with your actual domains. Point your reverse proxy at port 8080 for the Element Web subdomain (e.g., element.example.com).

Apply the changes:

docker compose up -d

Configuration

Enable Public Registration

By default, registration is disabled. To allow anyone to register on your server, edit homeserver.yaml:

enable_registration: true
enable_registration_without_verification: true

For production use, require email verification instead:

enable_registration: true
registrations_require_3pid:
  - email
email:
  smtp_host: smtp.example.com
  smtp_port: 587
  smtp_user: "[email protected]"
  smtp_pass: "smtp-password"
  notif_from: "Matrix <[email protected]>"

Restart Synapse after config changes:

docker compose restart synapse

Federation

Federation is enabled by default. For other servers to find yours, you need proper DNS and well-known delegation. If your server_name is example.com but Synapse runs on matrix.example.com, set up delegation with one of these methods:

Method 1: .well-known (recommended)

Serve this JSON at https://example.com/.well-known/matrix/server:

{
    "m.server": "matrix.example.com:443"
}

And this at https://example.com/.well-known/matrix/client:

{
    "m.homeserver": {
        "base_url": "https://matrix.example.com"
    }
}

Method 2: DNS SRV record

Create a DNS SRV record:

_matrix._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com.

Test federation with the Matrix Federation Tester.

Media Storage Limits

Control upload sizes and storage in homeserver.yaml:

# Maximum upload size in bytes (default 50MB)
max_upload_size: 50M

# Maximum image size for URL previews
max_image_pixels: 32M

# How long to keep remote media cached (default 90 days)
# Remote media is content fetched from other homeservers
media_retention:
  remote_media_lifetime: 90d

For servers with limited disk space, consider setting up media storage on a separate volume or enabling the media retention settings to automatically clean up old remote media.

Reverse Proxy

Synapse should sit behind a reverse proxy that handles TLS. Your reverse proxy must:

  • Proxy https://matrix.example.com to http://127.0.0.1:8008
  • Forward the X-Forwarded-For and X-Forwarded-Proto headers
  • Allow large request bodies (for file uploads): set client_max_body_size 50M in Nginx or equivalent
  • Proxy WebSocket connections for real-time sync

If you also host Element Web, add a second proxy rule for element.example.com pointing to http://127.0.0.1:8080.

For detailed reverse proxy configuration with Nginx Proxy Manager, Traefik, or Caddy, see Reverse Proxy Setup.

Backup

Critical data to back up:

  • PostgreSQL database — contains all messages, room state, user accounts, and encryption keys. This is the most important backup target.
  • Synapse data volume (synapse_data) — contains homeserver.yaml, media uploads, signing keys, and log configuration.

Database Backup

docker compose exec synapse_db pg_dump -U synapse synapse > synapse_backup_$(date +%Y%m%d).sql

Restore

docker compose exec -T synapse_db psql -U synapse synapse < synapse_backup_20260224.sql

Back up both the database and the data volume daily. Store backups off-server. See Backup Strategy for a complete 3-2-1 backup approach.

Troubleshooting

Federation Not Working

Symptom: Users on other Matrix servers cannot find or message your users. The Federation Tester shows errors.

Fix: Check these in order:

  1. Verify .well-known/matrix/server is accessible from the internet:
    curl https://example.com/.well-known/matrix/server
  2. Confirm port 8448 is open in your firewall (or port 443 if using well-known delegation).
  3. Check that your reverse proxy forwards traffic to Synapse correctly.
  4. Verify your TLS certificate is valid and not self-signed — federation requires trusted certificates.
  5. Run the Federation Tester and fix any reported issues.

Registration Disabled Error

Symptom: Users see “Registration has been disabled” when trying to create an account.

Fix: This is the default and intentional. Either:

  • Create users manually with register_new_matrix_user (see Initial Setup above)
  • Enable registration in homeserver.yaml by setting enable_registration: true and restart Synapse

Database Connection Errors

Symptom: Synapse logs show psycopg2.OperationalError: could not connect to server or similar PostgreSQL errors.

Fix:

  1. Verify the database container is running: docker compose ps synapse_db
  2. Check that host in the database section of homeserver.yaml matches the database service name (synapse_db).
  3. Confirm the username, password, and database name match between homeserver.yaml and your .env file.
  4. Ensure the database was initialized with the correct encoding:
    docker compose exec synapse_db psql -U synapse -c "SHOW server_encoding;"
    It must return UTF8. If not, delete the database volume and recreate it:
    docker compose down
    docker volume rm matrix-synapse_synapse_db_data
    docker compose up -d

Media Upload Failures

Symptom: Users cannot upload files or images. Errors about request body too large.

Fix:

  1. Check max_upload_size in homeserver.yaml (default is 50M).
  2. Your reverse proxy must also allow large request bodies. For Nginx, add client_max_body_size 50m; to the server block. For Caddy, set request_body max size.
  3. Check disk space on the server — if the data volume is full, uploads will fail:
    df -h
    docker system df

High Memory Usage

Symptom: Synapse consumes 2 GB+ of RAM and the server becomes unresponsive.

Fix: Synapse is known for high memory usage, especially on active servers. Mitigate it:

  1. Add connection pooling limits to the database config in homeserver.yaml:
    database:
      args:
        cp_min: 5
        cp_max: 10
  2. Limit the number of federation connections by adding to homeserver.yaml:
    federation_rr_transactions_per_room_per_second: 20
  3. Set up workers for large deployments (50+ active users). Synapse supports splitting work across multiple worker processes. See the Synapse workers documentation.
  4. Consider adding a Redis container for inter-worker communication if you enable workers.

Signing Key Issues After Migration

Symptom: Federation breaks after moving Synapse to a new server. Other servers reject your messages.

Fix: The signing key in /data/example.com.signing.key must be preserved across migrations. If you lost it, you will need to generate new keys and wait for other servers to pick up the change. Always include the entire /data directory in your backups.

Resource Requirements

  • RAM: 1 GB idle with no users. 2 GB minimum for a small deployment (under 20 users). 4 GB+ recommended for 100+ users. Synapse is memory-hungry — plan accordingly.
  • CPU: Low for small deployments. Medium for active servers with federation. CPU usage spikes during media processing and room state resolution.
  • Disk: 500 MB for the application itself. Media storage grows unbounded with usage — budget at least 20 GB and monitor growth. PostgreSQL database grows roughly 1 GB per 100,000 messages.

Verdict

Matrix Synapse is the best self-hosted chat platform for most people. Nothing else gives you decentralized federation, end-to-end encryption, bridging to other platforms (Slack, Discord, IRC, Telegram), and a mature ecosystem of clients. The Matrix protocol is an open standard, so you are never locked into a single implementation.

The trade-off is complexity. Synapse is harder to set up and run than simpler alternatives, and it uses more RAM than you would expect. Federation, while powerful, adds operational overhead — DNS delegation, certificate management, and debugging inter-server communication are not trivial.

If you just need a Slack replacement for your team and do not care about federation or bridging, Mattermost is simpler to deploy and lighter on resources. If you want the full power of decentralized, encrypted communication with the ability to talk to anyone on the Matrix network, Synapse is the clear choice.