Background
Deploying a Matrix chat service on a cloud server with 2 GB of RAM to receive operational reports and alerts from a VM cluster. Core requirements:
- Low memory footprint (other services are already running on the server)
- No ICP filing required (domain is unfiled)
- Support for receiving messages via Element clients on mobile/desktop
- Integration capability with an existing NATS messaging system
Environment
- Server: 2 vCPUs / 2 GB RAM / 50 GB disk / Debian 12
- Existing services: Uptime Kuma, Forgejo Runner, Node Exporter (consuming ~640 MB)
- Domain is unfiled
- Cloudflare Tunnel is already running
Solution Selection
Matrix Homeserver: Conduit
Three options were compared:
| Solution | Memory Usage | Maintenance Status | Suitable Use Case |
|---|---|---|---|
| Synapse (Python) | 500 MB+ | Officially maintained | Large-scale deployments |
| Dendrite (Go) | 100–200 MB | Officially maintained | Medium-scale deployments |
| Conduit (Rust) | 50–100 MB | Actively maintained by community fork | Low-memory, self-hosted use |
Conduit was selected because it’s the only option comfortable to run on a 2 GB server. Note: The original Conduit maintainer has stepped down; the active community fork Conduwuit (now renamed Grapevine) is recommended over the upstream version.
Networking: Cloudflare Tunnel
Since the domain is unfiled, direct DNS resolution to a domestic IP is not permitted. Cloudflare Tunnel resolves this:
- Traffic flows through an encrypted Cloudflare tunnel, never directly exposing the server IP
- No need to expose ports 80/443 — reducing the attack surface
- No need for Caddy/Nginx reverse proxying — Tunnel connects directly to Conduit
- Automatic HTTPS — no manual certificate management required
Bridge Bot: Python matrix-nio
A bridge program forwarding NATS messages into Matrix. Python was chosen due to its simplicity for this logic (subscribe → format → send), where Go-level performance is unnecessary.
Deployment Steps
1. Conduit Configuration and Startup
Create configuration file:
# /var/lib/conduit/conduit.toml
[global]
server_name = "chat.example.com"
database_path = "/var/lib/conduit/data"
database_backend = "rocksdb"
port = 6167
address = "0.0.0.0" # Must be 0.0.0.0 inside container, not 127.0.0.1
allow_registration = false # Disable public registration
allow_federation = false # Federation unnecessary for self-use
max_request_size = 20_000_000
log = "warn,rocket=off,_=off,sled=off"
Start container:
mkdir -p /var/lib/conduit/data
chown -R 1000:1000 /var/lib/conduit
podman run -d --name conduit --restart=always \
-v /var/lib/conduit:/var/lib/conduit:z \
-e CONDUIT_CONFIG=/var/lib/conduit/conduit.toml \
-p 127.0.0.1:6167:6167 \
docker.io/matrixconduit/matrix-conduit:latest
Generate systemd service and apply memory limits:
podman generate systemd --name conduit --new --restart-policy=always \
> /etc/systemd/system/conduit.service
mkdir -p /etc/systemd/system/conduit.service.d
cat > /etc/systemd/system/conduit.service.d/memory.conf << EOF
[Service]
MemoryMax=200M
MemoryHigh=150M
EOF
systemctl daemon-reload && systemctl enable conduit
Pitfall: In Conduit’s config, address must be 0.0.0.0. Using 127.0.0.1 fails because the container’s loopback interface is isolated from the host’s — port mapping becomes unreachable.
2. Cloudflare Tunnel Routing
In the Cloudflare Zero Trust dashboard, add a Public Hostname:
- Subdomain:
chat, Domain:example.com - Service:
http://localhost:6167
The Tunnel agent automatically creates the corresponding CNAME record. Verify:
curl -s https://chat.example.com/_matrix/client/versions
# Should return {"versions":["r0.5.0","r0.6.0","v1.1",...]}
3. Create User Accounts
The first registered user in Conduit automatically becomes an administrator. Temporarily enable registration, create accounts, then disable it again:
# Temporarily set allow_registration = true, restart container
# Register admin account
curl -X POST https://chat.example.com/_matrix/client/r0/register \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":\"YOUR_STRONG_PASSWORD\",\"auth\":{\"type\":\"m.login.dummy\"}}"
# Register bot account (same steps, different username)
# Revert allow_registration = false, restart container
4. NATS–Matrix Bridge Bot
Core logic (Python, ~180 lines):
import asyncio, json, ssl
import nats
from nio import AsyncClient
class Bridge:
async def run(self):
# 1. Log into Matrix
await self.matrix.login(password)
# 2. Join or create room
await self.ensure_room()
# 3. Connect to NATS JetStream (TLS + nkey)
tls_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
tls_ctx.load_verify_locations(ca_file)
tls_ctx.load_cert_chain(cert, key)
self.nc = await nats.connect(url, tls=tls_ctx)
# 4. Subscribe and forward
js = self.nc.jetstream()
for subject in subjects:
sub = await js.subscribe(subject, durable=f"bridge-{subject}")
asyncio.create_task(self._consume(sub))
async def _consume(self, sub):
async for msg in sub.messages:
data = json.loads(msg.data)
text = self.format_message(data)
await self.matrix.room_send(room_id, "m.room.message",
{"msgtype": "m.text", "body": text})
await msg.ack()
Example message formatting (operational report):
📊 server-01 Operational Report
✅ Completed: Database backup, log cleanup, SSL renewal
🔄 In progress: Performance optimization, API integration
💻 Disk usage: 14% | Memory usage: 16%
😊 Satisfaction rating: 3/5
Managed via systemd, with MemoryMax=100M and Restart=on-failure.
5. Connecting Element Clients
Open Element (desktop/mobile/Web), enter your custom Homeserver URL during login, join the designated room — push notifications will arrive immediately.
Pitfalls Encountered
- Conduit
addressconfiguration: Must listen on0.0.0.0inside the container —127.0.0.1breaks port mapping. - Python 3.14 TLS strict validation: Self-signed CAs missing
key usageextensions are rejected; adjustssl.SSLContext.verify_mode. - NATS JetStream subject matching: Confirm actual subjects present in the stream before subscribing — names may differ from expectations.
- Running venv Python under systemd: Direct symlinks to venv’s
pythonbinary may cause203/EXECfailures; use a wrapper shell script that sourcesactivatebeforeexec python. grepoverridden byrgalias: In scripts, usecommand grep -Eto bypass shell aliases.
Resource Usage
Actual memory consumption after deployment:
| Service | Memory |
|---|---|
| Conduit | ~80 MB |
| Bridge Bot | ~39 MB |
Total new memory overhead ≈ 120 MB — comfortably within the 2 GB limit.
Validation Results
- Matrix API responds correctly, supporting protocol versions up to
v1.12 - Element clients successfully log in and exchange messages
- End-to-end flow verified: Reports sent from any node → NATS → Bridge → Matrix → Element
- Registration disabled; federation disabled
- systemd services auto-start at boot; memory limits enforced
Summary
This solution is ideal for self-hosted Matrix deployments on low-memory servers. The combination of Conduit + Cloudflare Tunnel is especially well-suited for unfiled domains in mainland China, adding only ~120 MB of memory overhead. If future needs arise — e.g., federation or end-to-end encryption — consider migrating to Conduwuit (Grapevine) or Dendrite.