How to Host Matrix Synapse for VPS Without Wahala
Learn wetin Synapse need for VPS: 1 vCPU and 2 GB RAM for small homeserver, plus Postgres, media pruning, registration defence, and complete backups.
Wetin e take to keep Matrix Synapse homeserver alive
Matrix Synapse easy to install and easy to neglect. The installation na one apt repository, one config file, one reverse proxy block, and one DNS record. To keep the homeserver healthy for one year na different work: real database, media store wey you dey prune, registration wey strangers no fit use, and backup wey capture both parts of the server.
This guide target Ubuntu 24.04 LTS and e install Synapse from matrix.org apt repository. Na the package source wey Synapse project dey maintain for Debian and Ubuntu. Package versions dey change every few weeks, so no version number dey quoted here. Every path and option below come from current Synapse documentation.
Sizing: wetin 1 vCPU and 2 GB of RAM really fit handle
Published sizing pages, as of August 2026, commonly put one Synapse homeserver for 1 vCPU and 2 GB of RAM. This one honest for one situation: private server, few users, small rooms, and no busy public rooms. Synapse documentation talk clearly about the other situation. E ask for "At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org". This free RAM dey on top Python, Postgres, and the kernel memory.
One room fit change your sizing because of how joining dey work. When local user join room, your homeserver become full participant for that room. E receive every event from every other server wey dey inside, e verify the signature for each one, and e store the room state locally. Large public room fit get thousands of members spread across hundreds of servers. So your machine go do this work continuously, whether your user ever open the room again or not. If user later leave the room, e no delete the history wey you don already store.
Most of Synapse RAM dey go to caches. The caches section get a global_factor wey dey scale every cache at once, and the SYNAPSE_CACHE_FACTOR environment variable dey set the same thing. If you increase am, you dey use RAM to avoid database queries. If you reduce am, you dey use more CPU and Postgres time to save RAM. Postgres need memory for itself too, so for 2 GB machine, both of dem dey compete for the same megabytes.
Two practical rules for small plan. Add swap: swap no go make Synapse fast, but e go stop the kernel from killing the process during a large join. Then monitor disk from the first week, because the two things wey fit grow without limit na the media store and room state tables, and both dey live for disk.
Why you need Postgres, and why SQLite no longer dey work
The Debian package dey start with SQLite. E okay for first boot, but e no correct for server wey other people dey use. SQLite allow only one writer at a time. Federation traffic and client requests fit write for the same time, so one cheap request go wait behind one slow request. The symptom wey your users go report na say the app dey hang for some seconds at random.
The second reason dey come from the design. Synapse worker processes na the supported way to use more than one CPU core, and workers need Postgres. If you remain on SQLite, you lose both the upgrade path and the performance benefit.
You fit migrate later, but migration go cause downtime. So do am before users start to use the server. Synapse ships synapse_port_db, wey dey copy SQLite database enter prepared Postgres database:
synapse_port_db --sqlite-database homeserver.db --postgres-config homeserver-postgres.yamlIf you prefer to run the database for container beside Synapse, the trade-offs dey for running your database for Docker or for the host.
Install Synapse for Ubuntu 24.04
sudo apt install -y lsb-release wget apt-transport-https
sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/matrix-org.list
sudo apt update
sudo apt install matrix-synapse-py3For Ubuntu 24.04, lsb_release -cs dey print noble, and matrix.org repository dey publish a noble suite. No use the matrix-synapse package from Ubuntu own archive. Synapse project dey ask make you no use am, because those builds dey behind its releases and get known security bugs.
The installer go ask for server name and write your answer to /etc/matrix-synapse/conf.d/server_name.yaml. Answer am carefully. server_name na the part after the colon for every user ID (@alice:example.com), and dem dey put am inside every room wey your server create. If you change am later, e no go move anything: e go produce another homeserver. Use your bare domain, example.com, even when Synapse itself go run for matrix.example.com. Delegation dey connect the two, and na that the next section go explain.
The package dey run Synapse as the matrix-synapse user, keep its data under /var/lib/matrix-synapse, and read /etc/matrix-synapse/homeserver.yaml followed by every file inside /etc/matrix-synapse/conf.d/. Put your own settings for small files inside conf.d. Package upgrades no go touch dem.
sudo systemctl restart matrix-synapse
systemctl status matrix-synapse
sudo journalctl -u matrix-synapse -n 100 --no-pagerWhen startup healthy, the listeners go come up, then the service go quiet. The systemd unit go restart the service few seconds after any exit, so if Synapse reject the config, the unit go start and die repeatedly. The last lines for the journal go name the key wey e reject.
Point Synapse go Postgres
sudo apt install -y postgresql
sudo -u postgres createuser --pwprompt synapse_user
sudo -u postgres createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse_user synapseLocale no be decoration. Synapse no go start against database wey dem create with different COLLATE and CTYPE values unless you set allow_unsafe_locale for database config. The documented repair afterwards na dump and reload into database wey dem create correctly. Create am correctly the first time.
database:
name: psycopg2
txn_limit: 10000
args:
user: synapse_user
password: secretpassword
dbname: synapse
host: localhost
port: 5432
cp_min: 5
cp_max: 10Keep exactly one database: key for all your config files. Replace the SQLite block inside homeserver.yaml instead of adding another copy under conf.d, so nobody go confuse which one dey active. Restart, then prove say Synapse really dey use Postgres:
sudo -u postgres psql synapse -c "SELECT count(*) FROM users;"A number mean say Synapse build im schema for this database. Error about missing relation mean say e still dey write to the SQLite file, so the config wey you edit no be the one wey e dey read.
Reverse proxy, TLS and the .well-known files wey federation need
Synapse dey listen for plain HTTP on port 8008, and e bind to localhost. Reverse proxy wey dey in front na im own TLS and public port.
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses:
- '::1'
- '127.0.0.1'
resources:
- names:
- client
- federation
compress: falsex_forwarded: true dey tell Synapse make e trust the X-Forwarded-For header wey proxy set. If you no set am, every client go look like say e come from 127.0.0.1. Rate limiting go then see one local user wey dey too busy and throttle everybody together.
location ~ ^(/_matrix|/_synapse/client) {
proxy_pass http://localhost:8008;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host:$server_port;
client_max_body_size 50M;
proxy_http_version 1.1;
}Synapse documentation get one warning about this block wey don make people waste days. No add path after the port for proxy_pass, not even one /. nginx go then canonicalise the URI. This one go change the bytes wey sending server sign, and federation requests go fail signature verification, while ordinary client requests still dey work.
client_max_body_size must at least match Synapse's max_upload_size. If nginx get the smaller value, uploads wey pass am go get rejected by nginx with 413 Request Entity Too Large before Synapse see dem. So Synapse log no go get any line wey explain the failure.
For the certificate, follow Certbot and Let's Encrypt for Ubuntu 24.04. If you never choose reverse proxy, the reverse proxy comparison explain which one fit handle TLS for you.
Delegation na wetin allow server_name to remain example.com while Synapse dey run for matrix.example.com. Serve two files from the bare domain:
location /.well-known/matrix/server {
default_type application/json;
return 200 '{"m.server": "matrix.example.com:443"}';
}
location /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin '*';
return 200 '{"m.homeserver": {"base_url": "https://matrix.example.com"}}';
}The server file dey tell other homeservers where to send federation traffic. Na so federation fit run over 443 instead of the default port 8448. The client file dey tell Matrix clients which URL dey back @alice:example.com. The Access-Control-Allow-Origin header matter for the client file because browser-based clients dey fetch am cross-origin. Without the header, browser go block the response and the client go report say e no fit find your homeserver.
Both files must come through valid TLS from example.com itself. Check dem, then check wetin people outside dey see:
curl -s https://example.com/.well-known/matrix/server
curl -s https://matrix.example.com/_matrix/federation/v1/versionThe first one returns the JSON wey you write. The second one returns a JSON object wey names the server implementation and its version. This proves say proxy dey reach Synapse through the federation path. Then run the domain through the Matrix federation tester for https://federationtester.matrix.org. E follows the same route wey real remote server dey take.
Federate or no federate: decide am well
Federation na the main reason Matrix dey exist, and na also where most of the cost dey come from. Federating homeserver dey accept connections from servers wey you never hear about, receive their events, cache their media, and store state for every room wey your users touch. This na threat model decision, no be default setting.
Federate when your users need reach people for other homeservers, or when portable identity na the reason you choose Matrix. No federate when the server dey serve one team and every account for there na your own. Closed server dey store less, receive less, and e no too attract abuse.
To restrict federation instead of disabling am, Synapse dey use allow list:
federation_domain_whitelist:
- lon.example.com
- nyc.example.comThe documentation recommend say you firewall the federation listener too, so unwanted traffic go stop for the network instead of inside Python. To switch federation off completely, remove federation from the listener resources list, no publish /.well-known/matrix/server, and leave port 8448 closed.
If private team chat na the reason you dey run Matrix and federation never dey part of the plan, compare the running cost with other self-hosted Slack alternatives before you commit to Synapse. Rocket.Chat for Docker Compose fit handle team chat for smaller machine, because e never need store another organisation room state.
Media repository na the thing wey dey quietly full the disk
Files wey your own users upload dey stay for your disk permanently. Files wey users for other homeservers post dey fetch and cache for your disk as soon as one of your clients displays dem. Synapse still dey generate thumbnails for images, so one photo fit become several files. By default, nothing dey expire.
Find the store and measure am:
grep media_store_path /etc/matrix-synapse/homeserver.yaml
sudo du -sh /var/lib/matrix-synapse/media_storeMeasure the path wey your own config prints. Debian package dey keep Synapse data under /var/lib/matrix-synapse, so the store normally dey there. Then set retention policy for conf.d:
media_retention:
local_media_lifetime: 90d
remote_media_lifetime: 14dRead those two lines well, because dem no be the same kind of setting. remote_media_lifetime expires cache, and anything wey e delete fit fetch again from the server wey owns the file. local_media_lifetime permanently deletes uploads from your own users once dem reach that age. Team wey dey share documents for chat and expect to find dem next year go lose dem. Many servers set only the remote value.
For one-time cleanup, admin API dey take Unix timestamp for milliseconds:
BEFORE_TS=$(date -d '30 days ago' +%s%3N)
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://matrix.example.com/_synapse/admin/v1/purge_media_cache?before_ts=$BEFORE_TS"POST /_synapse/admin/v1/purge_media_cache dey drop cached remote media wey dem last access before that timestamp. POST /_synapse/admin/v1/media/delete?before_ts=<ms> deletes local media by the same rule. Run the remote purge first and measure again, because for federating server, remote cache normally dey make up the larger half.
Two settings dey contribute to the same disk usage. max_upload_size limits one upload and e must stay in step with client_max_body_size for nginx. url_preview_enabled: true makes your server fetch remote pages so clients fit show link previews. This dey use bandwidth and store thumbnails for content wey nobody upload to you.
Close registration before somebody discover your homeserver
Scanners fit find open homeserver within days. Once anybody fit create account, your server go become spam source for every room wey e federate with, and the administrators for the other side go block your whole domain. That reputation damage go last pass the cleanup, because people dey maintain the block lists by hand.
Synapse ships closed. enable_registration default na false and registration_requires_token default na false. Synapse also refuse to start when registration dey enabled but no verification step dey, unless you additionally set enable_registration_without_verification: true. That refusal na deliberate, so no switch am on just to make startup error disappear.
Create the accounts wey you want by hand:
sudo register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008E go ask for the user name, the password, and whether the account na server admin. E dey read registration_shared_secret from the config wey you pass with -c, so if e report say e no fit find shared secret, point -c to the file wey hold am.
When creating accounts by hand no longer scale, registration tokens na the setting between both options. Token na string wey new user must present during signup, and each token fit get limit for how many times e go work:
enable_registration: true
registration_requires_token: truecurl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"uses_allowed": 1}' \
https://matrix.example.com/_synapse/admin/v1/registration_tokens/newLeave token out of the body and Synapse go generate one and return am. GET /_synapse/admin/v1/registration_tokens go list the tokens wey still dey active. Both calls need the access token of a server admin account. You go get am by logging in as the admin user wey you create above.
Organisation wey already dey manage accounts elsewhere fit skip local passwords completely, because Synapse fit delegate login to OIDC (OpenID Connect) provider, for example Authentik as self-hosted SSO provider. Joiners and leavers go then dey handled for one place.
Backup wey fit actually rebuild the server
Synapse backup get three parts, and if backup miss any one of dem, e go restore server wey nobody fit use.
- The Postgres database, wey hold every event, account and room.
- The media store directory, wey hold every uploaded file.
/etc/matrix-synapse, wey hold your config and the server signing key.
The signing key na the part wey people dey forget. Na the private key your homeserver dey use sign events, and remote servers dey verify the events against the matching public key. Run grep signing_key_path /etc/matrix-synapse/homeserver.yaml to see where your own dey. If you lose am, you go restore server wey no fit prove say na the same server wey your rooms already know.
sudo -u postgres pg_dump --format=custom --file=/var/backups/synapse-$(date +%F).dump synapse
sudo tar czf /var/backups/synapse-etc-$(date +%F).tgz -C /etc matrix-synapseDump the database first, then copy the media store. Media files dey write once and reference by ID, so media copy wey you take after the dump fit only get extra files; e no fit miss files. If you do am the other way, the restored database fit point to file wey your backup never capture.
Send all three parts comot from the VPS. restic with off-site snapshots fit this arrangement well, because the media store na the bigger part and e barely dey change between runs. So deduplication dey keep each snapshot small.
Then practise the restore, because backup wey you never restore na only assumption. Build second VPS, install the same package, restore the config, create the database with the same encoding and locale, pg_restore the dump enter am, copy the media store back, and log in. Write down how long e take. That number na your real recovery time.
When state tables dey grow: compaction
Synapse dey store room state as state groups, and for server wey dey federate, state_groups_state often dey become the biggest object for the database. Measure first before you change anything:
sudo -u postgres psql synapse -c "SELECT pg_size_pretty(pg_database_size('synapse'));"
sudo -u postgres psql synapse -c "SELECT pg_size_pretty(pg_total_relation_size('state_groups_state'));"If na that one table dey make up most of your database, the project publish compressor for am, rust-synapse-compress-state. E dey rewrite the state group hierarchy into fewer rows without changing wetin any room state mean. Dem build am with Rust:
sudo apt install -y build-essential libssl-dev pkg-config git
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
git clone https://github.com/matrix-org/rust-synapse-compress-state.git
cd rust-synapse-compress-state/synapse_auto_compressor
cargo build --release./target/release/synapse_auto_compressor -p postgresql://synapse_user:secretpassword@localhost/synapse -c 500 -n 100-c na how many state groups e dey process at once, and -n na how many of those chunks this run go process. The auto compressor dey record how far e reach, so the next run go continue from there. Na this one make am safe to schedule. The documentation talk say e dey apply the changes inside transactions against append-only tables, so e fit run while Synapse still dey up. Still, make you back up the database before the first run.
One Postgres detail dey surprise people for here. When you delete rows, Postgres go keep the freed space for reuse; e no return am to the file system. So df fit no reduce at all after large compaction. VACUUM FULL go return the space, but e dey take exclusive lock on the table and free disk wey roughly equal to the table size. So schedule am as maintenance; no just run am anyhow.
Checks wey go tell you say server dey healthy
systemctl status matrix-synapse
curl -s https://example.com/.well-known/matrix/server
curl -s https://matrix.example.com/_matrix/federation/v1/version
sudo -u postgres psql synapse -c "SELECT pg_size_pretty(pg_database_size('synapse'));"
sudo du -sh /var/lib/matrix-synapse/media_storeHealthy mean say the unit dey active and e no dey restart, the delegation file dey return your m.server value, the federation version endpoint dey return JSON, and you fit compare the two size numbers with last month own. The size checks na the ones wey people dey skip, but disk na the failure wey fit bring Synapse server down without warning: when volume full, Postgres no fit write again, and Synapse go then fail every request wey touch the database.
FAQ
How much RAM Matrix Synapse server need?
For private homeserver wey get small number of users, small rooms, and no big public rooms, 2 GB dey workable. Na wetin most published sizing pages recommend as of August 2026. Synapse documentation ask for at least 1 GB free RAM on top the remaining memory if your users go join big public rooms like #matrix:matrix.org. This one happen because your server go store that room state and process its traffic continuously. Add swap for 2 GB plan so one big join no make kernel kill the process.
I must use PostgreSQL instead of SQLite?
After small number of users, yes. SQLite allow only one writer at a time. So federation traffic and client requests go block each other under load, and requests fit hang for some seconds. Synapse worker processes, wey na the supported way to use more than one CPU core, require Postgres. You fit migrate later with synapse_port_db, but e go cause downtime. So create the database with --encoding=UTF8 --locale=C --template=template0 before users start use the server.
Why Synapse disk usage dey continue grow?
Na one directory and one table. Media store dey keep every file wey users upload to rooms wey your server dey inside, including cached copies of remote users' media and generated thumbnails. Nothing go expire until you set media_retention. The state_groups_state table dey grow with room state for federating server, and rust-synapse-compress-state dey reduce am. Measure both with du -sh on your media_store_path and with SELECT pg_size_pretty(pg_total_relation_size('state_groups_state')); before you decide which one to work on.
How I fit stop strangers from registering on my homeserver?
Leave enable_registration for its default value of false and create accounts with register_new_matrix_user. When this method no longer scale, set enable_registration: true together with registration_requires_token: true, then give users tokens wey you create through POST /_synapse/admin/v1/registration_tokens/new. No set enable_registration_without_verification: true just to stop Synapse startup refusal. Open homeserver fit become spam source, and other administrators fit block your whole domain because of am.
My homeserver suppose federate?
Federation na decision about exposure, no be default setting. Use federation if your users need reach people for other homeservers. Keep am off if server dey serve one team, because non-federating server dey store less data, receive less traffic, and attract much less abuse. For the middle option, federation_domain_whitelist limit federation to named partner domains. Synapse documentation also recommend say you firewall the federation listener, instead make you rely only on that application-layer check.