$ mkdir -p /opt/matrix-synapse && cd /opt/matrix-synapse
mkdir -p /opt/matrix-synapse && cd /opt/matrix-synapse
mkdir -p /opt/matrix-synapse && cd /opt/matrix-synapse
# .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
# .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
# .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
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
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
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
services: synapse: image: matrixdotorg/synapse:v1.149.1 container_name: synapse -weight: 500;">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", "-weight: 500;">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 -weight: 500;">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
services: synapse: image: matrixdotorg/synapse:v1.149.1 container_name: synapse -weight: 500;">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", "-weight: 500;">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 -weight: 500;">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
services: synapse: image: matrixdotorg/synapse:v1.149.1 container_name: synapse -weight: 500;">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", "-weight: 500;">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 -weight: 500;">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
-weight: 500;">docker compose run --rm -e SYNAPSE_SERVER_NAME=example.com -e SYNAPSE_REPORT_STATS=no synapse generate
-weight: 500;">docker compose run --rm -e SYNAPSE_SERVER_NAME=example.com -e SYNAPSE_REPORT_STATS=no synapse generate
-weight: 500;">docker compose run --rm -e SYNAPSE_SERVER_NAME=example.com -e SYNAPSE_REPORT_STATS=no synapse generate
# Copy the config out of the volume
-weight: 500;">docker compose cp synapse:/data/homeserver.yaml ./homeserver.yaml
# Copy the config out of the volume
-weight: 500;">docker compose cp synapse:/data/homeserver.yaml ./homeserver.yaml
# Copy the config out of the volume
-weight: 500;">docker compose cp synapse:/data/homeserver.yaml ./homeserver.yaml
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
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
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
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
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
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
-weight: 500;">docker compose cp ./homeserver.yaml synapse:/data/homeserver.yaml
-weight: 500;">docker compose cp ./homeserver.yaml synapse:/data/homeserver.yaml
-weight: 500;">docker compose cp ./homeserver.yaml synapse:/data/homeserver.yaml
-weight: 500;">docker compose run --rm synapse chown 991:991 /data/homeserver.yaml
-weight: 500;">docker compose run --rm synapse chown 991:991 /data/homeserver.yaml
-weight: 500;">docker compose run --rm synapse chown 991:991 /data/homeserver.yaml
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
-weight: 500;">docker compose ps
-weight: 500;">docker compose ps
-weight: 500;">docker compose ps
-weight: 500;">curl http://localhost:8008/_matrix/client/versions
-weight: 500;">curl http://localhost:8008/_matrix/client/versions
-weight: 500;">curl http://localhost:8008/_matrix/client/versions
-weight: 500;">docker compose exec synapse register_new_matrix_user -u admin -p your-secure-password -a -c /data/homeserver.yaml http://localhost:8008
-weight: 500;">docker compose exec synapse register_new_matrix_user -u admin -p your-secure-password -a -c /data/homeserver.yaml http://localhost:8008
-weight: 500;">docker compose exec synapse register_new_matrix_user -u admin -p your-secure-password -a -c /data/homeserver.yaml http://localhost:8008
element: image: vectorim/element-web:v1.12.12 container_name: element -weight: 500;">restart: unless-stopped volumes: - ./element-config.json:/app/config.json:ro ports: - "127.0.0.1:8080:80" networks: - matrix
element: image: vectorim/element-web:v1.12.12 container_name: element -weight: 500;">restart: unless-stopped volumes: - ./element-config.json:/app/config.json:ro ports: - "127.0.0.1:8080:80" networks: - matrix
element: image: vectorim/element-web:v1.12.12 container_name: element -weight: 500;">restart: unless-stopped volumes: - ./element-config.json:/app/config.json:ro ports: - "127.0.0.1:8080:80" networks: - matrix
{ "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"
}
{ "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"
}
{ "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"
}
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
enable_registration: true
enable_registration_without_verification: true
enable_registration: true
enable_registration_without_verification: true
enable_registration: true
enable_registration_without_verification: true
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]>"
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]>"
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]>"
-weight: 500;">docker compose -weight: 500;">restart synapse
-weight: 500;">docker compose -weight: 500;">restart synapse
-weight: 500;">docker compose -weight: 500;">restart synapse
{ "m.server": "matrix.example.com:443"
}
{ "m.server": "matrix.example.com:443"
}
{ "m.server": "matrix.example.com:443"
}
{ "m.homeserver": { "base_url": "https://matrix.example.com" }
}
{ "m.homeserver": { "base_url": "https://matrix.example.com" }
}
{ "m.homeserver": { "base_url": "https://matrix.example.com" }
}
_matrix._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com.
_matrix._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com.
_matrix._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com.
# 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
# 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
# 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
-weight: 500;">docker compose exec synapse_db pg_dump -U synapse synapse > synapse_backup_$(date +%Y%m%d).sql
-weight: 500;">docker compose exec synapse_db pg_dump -U synapse synapse > synapse_backup_$(date +%Y%m%d).sql
-weight: 500;">docker compose exec synapse_db pg_dump -U synapse synapse > synapse_backup_$(date +%Y%m%d).sql
-weight: 500;">docker compose exec -T synapse_db psql -U synapse synapse < synapse_backup_20260224.sql
-weight: 500;">docker compose exec -T synapse_db psql -U synapse synapse < synapse_backup_20260224.sql
-weight: 500;">docker compose exec -T synapse_db psql -U synapse synapse < synapse_backup_20260224.sql
-weight: 500;">curl https://example.com/.well-known/matrix/server
-weight: 500;">curl https://example.com/.well-known/matrix/server
-weight: 500;">curl https://example.com/.well-known/matrix/server
-weight: 500;">docker compose exec synapse_db psql -U synapse -c "SHOW server_encoding;"
-weight: 500;">docker compose exec synapse_db psql -U synapse -c "SHOW server_encoding;"
-weight: 500;">docker compose exec synapse_db psql -U synapse -c "SHOW server_encoding;"
-weight: 500;">docker compose down -weight: 500;">docker volume rm matrix-synapse_synapse_db_data -weight: 500;">docker compose up -d
-weight: 500;">docker compose down -weight: 500;">docker volume rm matrix-synapse_synapse_db_data -weight: 500;">docker compose up -d
-weight: 500;">docker compose down -weight: 500;">docker volume rm matrix-synapse_synapse_db_data -weight: 500;">docker compose up -d
df -h -weight: 500;">docker system df
df -h -weight: 500;">docker system df
df -h -weight: 500;">docker system df
database: args: cp_min: 5 cp_max: 10
database: args: cp_min: 5 cp_max: 10
database: args: cp_min: 5 cp_max: 10
federation_rr_transactions_per_room_per_second: 20
federation_rr_transactions_per_room_per_second: 20
federation_rr_transactions_per_room_per_second: 20 - 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 - -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 - 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 - 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. - Verify .well-known/matrix/server is accessible from the internet: - Confirm port 8448 is open in your firewall (or port 443 if using well-known delegation).
- Check that your reverse proxy forwards traffic to Synapse correctly.
- Verify your TLS certificate is valid and not self-signed -- federation requires trusted certificates.
- Run the Federation Tester and fix any reported issues. - Create users manually with register_new_matrix_user (see Initial Setup above)
- Enable registration in homeserver.yaml by setting enable_registration: true and -weight: 500;">restart Synapse - Verify the database container is running: -weight: 500;">docker compose ps synapse_db
- Check that host in the database section of homeserver.yaml matches the database -weight: 500;">service name (synapse_db).
- Confirm the username, password, and database name match between homeserver.yaml and your .env file.
- Ensure the database was initialized with the correct encoding: - Check max_upload_size in homeserver.yaml (default is 50M).
- 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.
- Check disk space on the server -- if the data volume is full, uploads will fail: - Add connection pooling limits to the database config in homeserver.yaml: - Limit the number of federation connections by adding to homeserver.yaml: - Set up workers for large deployments (50+ active users). Synapse supports splitting work across multiple worker processes. See the Synapse workers documentation.
- Consider adding a Redis container for inter-worker communication if you -weight: 500;">enable workers. - 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. - Matrix vs Discord: Self-Hosted Chat Compared
- Matrix vs XMPP: Federated Chat Protocols Compared
- Matrix vs Zulip: Which Chat Server to Self-Host?
- Best Self-Hosted Communication & Chat
- Matrix Synapse vs Mattermost
- Matrix Synapse vs Rocket.Chat
- Self-Hosted Alternatives to Slack
- Self-Hosted Alternatives to Discord
- Docker Compose Basics
- Reverse Proxy Setup
- Backup Strategy