Deploying Matrix Chat Service on Low-Memory Servers: Complete Solution with Conduit + Cloudflare Tunnel + NATS Bridge

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

  1. Conduit address configuration: Must listen on 0.0.0.0 inside the container — 127.0.0.1 breaks port mapping.
  2. Python 3.14 TLS strict validation: Self-signed CAs missing key usage extensions are rejected; adjust ssl.SSLContext.verify_mode.
  3. NATS JetStream subject matching: Confirm actual subjects present in the stream before subscribing — names may differ from expectations.
  4. Running venv Python under systemd: Direct symlinks to venv’s python binary may cause 203/EXEC failures; use a wrapper shell script that sources activate before exec python.
  5. grep overridden by rg alias: In scripts, use command grep -E to 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.