> ## Content Index
> Fetch the complete content index at: https://blog.kyleelliott.net/llms.txt
> Use this file to discover other available public pages before exploring further.

# Self-Hosting a Matrix Server: What Actually Worked
- URL: https://blog.kyleelliott.net/self-hosting-a-matrix-server-what-actually-worked/
- Published: 2026-02-24T16:41:00.000Z
- Updated: 2026-02-24T16:41:00.000Z
- Description: I recently set up a private Matrix server on a 8GB VPS for my friends and family. Getting it running took more trial and error than I expected — so here's what the final setup looks like and what tripped me up along the way.
- Author: Kyle Elliott
- Tags: self-hosting, matrix, vps, docker

I recently set up a private Matrix server on a 4GB VPS for my family. Not because I enjoy pain, but because I wanted a self-hosted alternative. So many platforms are asking for too much personal information. Then storing that information in an insecure fashion and it ends up being leaked (I'm looking at you Discord).

Though not specifically related; when Google shut off API access to the Nest thermostat that I paid for it made me realize that platform dependency sucks. Since I have the ability I wanted to try self-hosting more systems in my day to day.

## The Stack

The core is **Synapse** (the Matrix reference homeserver) running behind **Caddy** as a reverse proxy, all wired together with Docker Compose. I also added a **Synapse Admin** container for GUI-based user management.

Coturn and Livekit for voice and video calling as well as the Livekit JWT service.

Caddy was the right call for the reverse proxy. It handles SSL certificates for your domain automatically; no Certbot configuration, no cron jobs. You point it at a domain and it figures out the rest.

The Synapse Admin UI sits at under 30MB of RAM, which on a 8GB VPS is practically free. It means I can reset passwords, manage users, and deactivate accounts without touching the terminal every time.

I can run all this alongside my existing portfolio, blog, and N8N systems with room to spare.

![a group of purple cubes hanging from a metal bar](https://images.unsplash.com/photo-1676911809746-85d90edbbe4a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGRpc3RyaWJ1dGVkfGVufDB8fHx8MTc3MTk1MTIyMnww&ixlib=rb-4.1.0&q=80&w=2000)

Photo by [Shubham Dhage](https://unsplash.com/@shubhudi?ref=blog.kyleelliott.net) / [Unsplash](https://unsplash.com/?utm%5Fsource=ghost&utm%5Fmedium=referral&utm%5Fcampaign=api-credit)

## Federation

Federation lets your server talk to the wider Matrix network — so your users can join rooms on `matrix.org` or message people on other homeservers. Getting this working required adding `.well-known` headers in the Caddyfile so other servers could find mine correctly.

One thing to know: joining a large public room for the first time kicks off a **State Resolution** process. For a smaller VPS, this can spike CPU and RAM hard enough to cause real problems. Join big rooms one at a time. Give the server time to settle before adding another.

## Getting the First Admin

This is the part that catches everyone. The Synapse Admin API requires an admin user to authenticate — but you can't promote a user to admin through the API until you already have one. Classic chicken-and-egg.

Two reliable paths out of that loop:

1. **`register_new_matrix_user`** — a CLI script included with Synapse that can create an admin user directly.
2. **Direct SQLite update** — modify the database on the mapped volume from the VPS host.

Important: modern Synapse Docker images are distroless. There's no `sqlite3` or `curl` inside the container. Run all database operations from the host machine using the mapped volume path.

After promoting a user via the database, don't expect it to work immediately. **You have to log out and back in.** The active session holds an old access token that doesn't reflect the new permissions. I wasted a while on this one.

Also: Matrix user IDs are **case-sensitive in the database**. If you run a SQL update with the wrong casing, it silently updates zero rows. Double-check the exact username as it appears in the registration record.

![Open padlock with combination lock on keyboard](https://images.unsplash.com/photo-1767972464040-8bfee42d7bed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8c2VhcmNofDI0fHxkaWdpdGFsJTIwcHJpdmFjeXxlbnwwfHx8fDE3NzE5NTEyNDB8MA&ixlib=rb-4.1.0&q=80&w=2000)

Photo by [Sasun Bughdaryan](https://unsplash.com/@sasun1990?ref=blog.kyleelliott.net) / [Unsplash](https://unsplash.com/?utm%5Fsource=ghost&utm%5Fmedium=referral&utm%5Fcampaign=api-credit)

## Keeping It Private

Synapse has an option for **registration tokens** — you generate a token, share the link with family, and anyone without it can't create an account. The server stays closed to the public without completely disabling registration. Disabling public registration keeps folks out that you would potentially need to verify yourself should the laws change.

## Performance Tuning

One config change made a noticeable difference: disabling global presence.

```yaml
use_presence: false

```

When you federate with large servers, presence tracking (knowing who's online/offline) generates a constant stream of traffic. Turning it off cuts a significant chunk of idle workload. For a small private server where everyone knows each other anyway, it's an easy trade.

## Nuance

When setting up the voice/video calls make sure you pay attention to the tokens that go between the JWT Livekit service, Livekit, and the homeserver.yaml files. That tripped me up more than once.

## Quick Reference: Gotchas

| Issue                                    |  | What to do                                  |
| ---------------------------------------- |  | ------------------------------------------- |
| Admin API not working after DB promotion |  | Log out and back in to get a fresh token    |
| sqlite3 not found in container           |  | Run from VPS host using mapped volume       |
| SQL update affected 0 rows               |  | Check username casing — it's case-sensitive |
| CPU spike joining a room                 |  | Join large rooms one at a time              |
| High idle load from federation           |  | Set use\_presence: false in config          |

---

This setup has been running cleanly since I got it sorted. My friends that have used it say that the voice chat quality is just as good as Discord's. Video chat works fine as well. If you're standing up something similar and hit a wall, feel free to drop a comment below.

Hopefully I won't regret all this later.