How to deploy Matrix on Kubernetes: A complete guide

March 13, 2026 
Denis ArnstDenis Arnst
 
KubernetesMatrixData SovereigntyGDPRSelf-HostedDiscord AlternativeOIDCLiveKitEnterprise Chat

Looking for a Discord replacement you can self-host? More importantly, are you an enterprise trying to escape the US Cloud Act and actually own your communications data?

If you handle sensitive data in the EU, platforms like Discord, Slack, and Microsoft Teams pose a hidden risk. Even if their servers are in Frankfurt, the companies are American. Under the Cloud Act, US authorities can compel them to hand over data regardless of physical location. Your infrastructure might be in Germany, but the legal jurisdiction is California.

With NIS2, DORA, and GDPR tightening the screws, enterprises are rethinking their dependence on US cloud providers. The question isn’t just “can we self-host?"—it’s “can we achieve compliance and feature parity with Teams without building a nightmare to maintain?”

The answer is Matrix. This guide covers the basics of the application, and explains how to deploy Matrix on Kubernetes.

Who This Is For

Whether you’re a compliance officer evaluating secure communication platforms or a platform engineer tasked with deploying one, this guide has you covered. It is structured in two layers: a decision section for leadership and a hands-on implementation section for engineers.

Decision makers (CIOs, compliance officers):

  • Read “The Decision Layer” to understand the compliance wins and the architecture strategy.
  • Use this to weigh Matrix against Wire, Mattermost, and Teams.

Platform engineers (DevOps, SREs):

  • Skip to “The Implementation Layer.” This assumes you know Kubernetes; it focuses on Matrix-specifics, OIDC glue, and production pitfalls.
  • Time investment: ~10 minutes to read, 4-6 hours to deploy.

Prerequisites to deploy Matrix on Kubernetes

To deploy Matrix on Exoscale Kubernetes, you’ll need:

  • Exoscale account with SKS access (Pro tier recommended for HA).
  • Kubernetes know-how (kubectl, Helm, Gateway API).
  • DNS control for your domain.
  • OIDC provider (Authentik, Keycloak, Authelia, Entra ID/Azure AD) - optional but recommended.

The Decision Layer: Why you should deploy Matrix on Kubernetes

Why Matrix? (It’s not just a chat app)

Matrix isn’t a “product”—it’s an open standard for secure, decentralized, real-time communication. Instead of relying on a single provider, it lets different servers (“homeservers”) federate—so users can message across domains much like email, while still supporting modern chat features like rooms, end-to-end encryption, and bridges to other networks. It’s battle-tested in high-stakes environments:

  • French Government (Tchap): Secure inter-ministerial comms.
  • German Military (BwMessenger): Operational messaging.
  • Swiss Post (ePost): Switzerland built a nationwide Matrix-based communication platform where all Swiss citizens can communicate with government departments, banks, hospitals, and insurance companies—deployed as national infrastructure.

The Landscape: Matrix vs. Discord vs. The Rest

For a clearer overview, we evaluated the available chat options based on data sovereignty, SSO support, and AV quality.

PlatformSelf-HostedJurisdictionOIDC/SSOVoice/VideoVerdict
Matrix (Synapse)⚠️ Server: EU / Client: UK✅ (via MAS)✅ Production (LiveKit)Best Balance
Matrix (Tuwunel)✅ Swiss⚠️ Basic✅ Production (LiveKit)Great for speed, lacks full Element X support
Wire✅ Swiss✅ (SAML)✅ ProductionExcellent but requires Cassandra etc. (complex)
MS Teams❌ Cloud ActThe Compliance Trap
Mattermost⚠️ BasicGood chat, weak video
Discord❌ Cloud Act✅ ExcellentFeature-rich, zero data sovereignty
Slack❌ Cloud Act✅ (SAML/OIDC)⚠️ Huddles onlyGreat UX, no sovereignty
FluxerDiscord clone, but not ready yet

Synapse vs. Tuwunel: Synapse (Python) is the reference Matrix homeserver maintained by Element, with the broadest feature support and the most mature MAS integration. Tuwunel (Rust) is a newer performance-focused homeserver funded by the Swiss government, offering lower resource usage but currently lacking full Element X delegated auth support (QR/device transfer). See the Field Notes section for a detailed comparison.

Teams looking to deploy Matrix on Kubernetes gain a key advantage: the ability to run a fully self-managed, GDPR-compliant communication platform on infrastructure they control entirely. Kubernetes makes it straightforward to scale, update, and operate Matrix components reliably—while Exoscale keeps the data within European jurisdiction.

The “Element” Nuance: Element is the most popular Matrix client, developed by Element (formerly New Vector), a UK-based company—not to be confused with the Matrix protocol itself. While your self-hosted homeserver (Synapse) sits safely in the EU on Exoscale, the Element client is maintained by a UK company. If absolute purity is required, you can use alternative clients (Nheko, FluffyChat), but Element offers the best enterprise experience.

Why not Wire? Wire is fantastic and fully Swiss. However, self-hosting it requires a complex stack involving Cassandra, Elasticsearch, and Minio. Matrix works with PostgreSQL and S3—things you probably already know how to manage. However, it can be still worth a look.

Why not Fluxer/Revolt? Fluxer looks great (very Discord-like) but lacks enterprise SSO. Revolt (Stoat) is similar, also lacks SSO.

Making Matrix Production-Ready on Exoscale Kubernetes

To make this production-ready on Exoscale, we separate concerns:

  1. Layer 1: Routing (Gateway API): We use Traefik via the Kubernetes Gateway API. It handles HTTP/S for the web client and Synapse API.
  2. Layer 2: Real-Time Media (UDP): Voice/Video needs raw UDP. We use dedicated Exoscale Network Load Balancers (NLB) for LiveKit and TURN. Do not try to proxy this through an Ingress; it will fail.
  3. Layer 3: Persistence (Managed DBaaS): Don’t run stateful workloads on K8s if you don’t have to. We use Exoscale Managed PostgreSQL and Valkey (Redis). It handles backups and HA for us.
  4. Layer 4: Identity (MAS): The Matrix Authentication Service bridges the gap between Matrix and your OIDC provider (Authentik/Keycloak).
                        Clients
         Element Web / Element X / Nheko / FluffyChat
                |  HTTPS                 |  HTTPS (OIDC)
    Layer 1: Traefik Gateway    Layer 4: MAS (auth)
    (NLB -> HTTP/S)             matrix-authentication-service
                \                      /
                 +--------------------+
                 |  Layer 3: Synapse  |
                 |  PostgreSQL+Valkey |
                 +--------+----------+
                          | UDP / RTP
                 Layer 2: Real-Time Media
                 LiveKit SFU (UDP NLB) + coturn TURN

The Implementation Layer: How to deploy Matrix on Exoscale Kubernetes

This section walks through 5 phases: (1) routing and gateway setup, (2) managed data layer, (3) core services (Synapse + MAS), (4) real-time media (LiveKit + TURN), and (5) production hardening. Each phase builds on the previous one.

Phase 1: Foundation & Routing

First, secure the perimeter for deploying Matrix on Kubernetes. We use the Gateway API because it handles multi-tenancy better than legacy Ingress.

1. Gateway Configuration You need a GatewayClass and a Gateway listener.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: matrix-gateway
  namespace: traefik
spec:
  gatewayClassName: traefik
  listeners:
  - name: web
    protocol: HTTP
    port: 80
    hostname: "*.example.com"
  - name: websecure
    protocol: HTTPS
    port: 443
    hostname: "*.example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - kind: Secret
        name: wildcard-tls  # cert-manager managed

HTTPRoute examples:

Matrix Synapse:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: matrix-synapse
  namespace: matrix
spec:
  parentRefs:
  - name: matrix-gateway
    namespace: traefik
    sectionName: web
  - name: matrix-gateway
    namespace: traefik
    sectionName: websecure
  hostnames:
  - matrix.example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: synapse
      port: 8008

MAS (OIDC). MAS (Matrix Authentication Service) is an OAuth 2.0 and OpenID Provider server for Matrix. All authentication (even your own OIDC provider!) runs through it. It is required for modern functionality like OIDC on the new Element app:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: mas
  namespace: matrix
spec:
  parentRefs:
  - name: matrix-gateway
    namespace: traefik
    sectionName: web
  - name: matrix-gateway
    namespace: traefik
    sectionName: websecure
  hostnames:
  - mas.example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: mas
      port: 8080

LiveKit (with JWT service):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: livekit-rtc
  namespace: matrix
spec:
  parentRefs:
  - name: matrix-gateway
    namespace: traefik
    sectionName: web
  - name: matrix-gateway
    namespace: traefik
    sectionName: websecure
  hostnames:
  - rtc.example.com
  rules:
  - matches:
    - path:
        type: Exact
        value: /sfu/get
    backendRefs:
    - name: lk-jwt  # JWT token service
      port: 8080
  - matches:
    - path:
        type: PathPrefix
        value: /livekit/jwt
    backendRefs:
    - name: lk-jwt
      port: 8080
  - matches:
    - path:
        type: PathPrefix
        value: /livekit/sfu
    backendRefs:
    - name: livekit-ws  # WebSocket signaling
      port: 7880
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: livekit-ws
      port: 7880

Namespace and Secrets

Create namespace and core secrets:

kubectl create namespace matrix

Required secrets:

  • POSTGRES_PASSWORD (database auth)
  • REGISTRATION_SHARED_SECRET (Synapse registration)
  • MACAROON_SECRET_KEY (Synapse auth tokens)
  • FORM_SECRET (Synapse CSRF protection)
  • TURN_SHARED_SECRET (TURN server auth)
  • MAS_SHARED_SECRET (MAS ↔ Synapse communication)
  • OIDC_CLIENT_ID, OIDC_CLIENT_SECRET (MAS ↔ IdP)

Security note: Use Kubernetes Secret objects, never commit secrets to git. For GitOps workflows, consider sealed-secrets or external secret operators.

# Example (use your own secure values)
kubectl create secret generic matrix-secrets \
  --from-literal=POSTGRES_PASSWORD='<secure-random>' \
  --from-literal=REGISTRATION_SHARED_SECRET='<secure-random>' \
  --from-literal=MACAROON_SECRET_KEY='<secure-random>' \
  --from-literal=FORM_SECRET='<secure-random>' \
  --from-literal=TURN_SHARED_SECRET='<secure-random>' \
  --from-literal=MAS_SHARED_SECRET='<secure-random>' \
  --namespace=matrix

Compliance checkpoint: Namespace isolation ensures Matrix workloads can’t access other cluster resources. This separation is critical for NIS2/DORA audit trails—each namespace has independent RBAC and network policies.


Phase 2: Data Layer

For production, we recommend Exoscale Managed DBaaS for PostgreSQL and Valkey. It reduces operational overhead (backups, patching, HA) and scales cleanly.

If you want full in‑cluster control, you can still run PostgreSQL and Valkey as StatefulSets backed by Exoscale Block Storage volumes.


Phase 3: Core Services

Synapse is the homeserver. MAS is the auth service.

Important: The new mobile client, Element X, mandates MAS if you want to use OIDC. If you want the modern mobile experience, you cannot skip MAS.

Synapse (Homeserver)

Key settings in homeserver.yaml:

server_name: "matrix.<domain>"
public_baseurl: "https://matrix.<domain>/"

redis:
  enabled: true
  host: matrix-valkey
  port: 6379

matrix_authentication_service:
  enabled: true
  endpoint: "http://mas:8080/"
  secret: "${MAS_SHARED_SECRET}"

turn_uris:
  - "turn:turn.<domain>:3478?transport=udp"
  - "turn:turn.<domain>:3478?transport=tcp"
turn_shared_secret: "${TURN_SHARED_SECRET}"
turn_user_lifetime: 1h

cors_allowed_origin_regex:
  - 'https://([a-z0-9-]+\.)?<domain>'
cors_allow_credentials: true

MAS (Matrix Authentication Service)

Synapse needs to know MAS exists to delegate auth.

MAS ↔ Synapse

This snippet goes in your MAS values.yaml (for Helm deployment). It tells MAS which Synapse instance to communicate with:

matrix:
  kind: synapse
  homeserver: "matrix.<domain>"
  endpoint: "http://synapse:8008"
  secret: "${MAS_SHARED_SECRET}"

Deploy MAS using the official charts.

Authentik OIDC provider

Authentik as an example. Configuration for other OIDC Providers like Keycloak is very similar.

upstream_oauth2:
  providers:
    - issuer: "https://auth.<domain>/application/o/matrix/"
      client_id: "${OIDC_CLIENT_ID}"
      client_secret: "${OIDC_CLIENT_SECRET}"
      token_endpoint_auth_method: "client_secret_basic"
      scope: "openid profile email"
      include_id_token: true
      claims_imports:
        skip_confirmation: true
        localpart:
          action: force
          template: "{{ (user.preferred_username or user.name) | lower | replace(' ', '-') }}"
          on_existing: add
        displayname:
          action: force
          template: "{{ user.name or user.preferred_username }}"
        email:
          action: ignore

MAS password login

passwords:
  enabled: false

Keep it disabled in production (OIDC‑only) unless you need local logins.


Element Web (Client)

Deploy Element Web behind the same Gateway (Traefik) and brand with a ConfigMap:

  • custom CSS
  • background image
  • logo override

Element reads /.well-known/matrix/client from Synapse to discover MAS and RTC.

Deploying Phase 3 Services

Each component has official deployment resources — use whichever approach fits your setup (raw manifests, Helm, or GitOps):

Once deployed, verify the pods are running:

kubectl get pods -n matrix

Phase 4: Real-Time Media (LiveKit & TURN)

LiveKit (MatrixRTC)

LiveKit is split into two traffic types:

1) WebSocket signaling (HTTPS)

  • Served via Gateway (Traefik) on rtc.<domain>

2) UDP media

This is mandatory for reliable voice and screen sharing.


TURN Server (NAT Traversal)

Use coturn behind a dedicated NLB with TCP + UDP. Keep the TURN shared secret in a Kubernetes Secret.


.well-known Configuration

Your Synapse /.well-known/matrix/client must include the RTC foci:

"org.matrix.msc4143.rtc_foci": [
  {"type": "livekit", "livekit_service_url": "https://rtc.<domain>"}
]

Phase 5: Production Hardening

S3 Media Storage (Optional)

Synapse supports S3 media storage using synapse-s3-storage-provider:

media_storage_providers:
  - module: s3_storage_provider.S3StorageProviderBackend
    store_local: true
    store_remote: true
    config:
      endpoint_url: https://sos-<zone>.exo.io
      access_key_id: <S3_ACCESS_KEY>
      secret_access_key: <S3_SECRET_KEY>
      bucket: <bucket>
      region_name: <zone>

This allows offloading large media blobs to SOS.


Federation Settings

Matrix supports federation between homeservers (like email between mail servers), but for most enterprise deployments you will want to configure it based on your use case:

  • Disable federation (recommended for internal deployments): maximizes data sovereignty, prevents metadata leakage to external Matrix servers. Set federation_domain_whitelist: [] in homeserver.yaml.
  • Enable selectively: useful for cross-organization collaboration, Matrix-based B2B integrations, or public-facing community spaces.
  • Whitelist specific domains: allow federation only with trusted partner organizations.

For 90% of enterprise deployments, disabling federation is the right default. Enable it selectively only when external collaboration is required and you understand the metadata trade-offs.


DNS Configuration

Point these to your Traefik Gateway NLB IP:

  • matrix.<domain>
  • mas.<domain>
  • space.<domain>
  • rtc.<domain>

LiveKit UDP and TURN use separate Exoscale NLB IPs.


Validation Checklist

  • https://matrix.<domain>/_matrix/client/versions → 200
  • https://matrix.<domain>/.well-known/matrix/client returns RTC foci

Field Notes & Troubleshooting

These are real-world lessons gathered from a production deployment of Matrix on Exoscale SKS in February 2026. The setup used Synapse as the primary homeserver, MAS for OIDC delegation, Authentik as the identity provider, and LiveKit for real-time media. We also tested Tuwunel as a drop-in Synapse replacement. Relevant upstream issue trackers: Synapse, MAS, Tuwunel.

Common Issues

CORS Blocked on .well-known

If Element shows a CORS error, update Synapse:

cors_allowed_origin_regex:
  - 'https://([a-z0-9-]+\\.)?example\\.com'
cors_allow_credentials: true

MISSING_MATRIX_RTC_FOCUS Error

This usually means /.well-known/matrix/client is missing rtc_foci or blocked by CORS.


Tuwunel + MAS Field Report (February 2026)

We tested replacing Synapse with Tuwunel, a Rust-based Matrix homeserver funded by the Swiss government and now deployed at scale serving Swiss citizens in production, on the same matrix.<domain> endpoint.

What worked:

  • Web SSO with Authentik can work on Tuwunel.
  • Element/Element X can reach the OIDC flow when .well-known points to MAS.
  • LiveKit and RTC routes kept working.

What required extra tuning:

  • Authentik redirect_uri must match exactly (no trailing slash mismatch).
  • Tuwunel OIDC issuer/discovery config must match Authentik discovery exactly.
  • If you route /_matrix/client/*/login to MAS compatibility endpoints, token exchange can fail depending on who issued the login token. We had to route carefully to avoid login token conflicts.

Known limitation we hit:

  • QR/device-transfer login was not supported reliably with Tuwunel + MAS in this setup.
  • MAS repeatedly expected Synapse-specific endpoints such as /_synapse/mas/sync_devices, which are not available on Tuwunel.

Practical recommendation:

  • If you need the full Element X delegated-auth experience including QR/device transfer, use Synapse + MAS.
  • If you can accept per-device web SSO login without QR transfer, Tuwunel can still be viable.

Discord Replacement UX Notes

Matrix can replace Discord well (voice, screen sharing, channels, mobile + desktop clients), but there is a UX difference:

  • Spaces don’t auto‑join all rooms. Users typically need to join each room once.

Mitigations:

  • Keep default rooms minimal
  • Use space‑restricted joins
  • Add a “Join all rooms” onboarding message
  • Auto‑join the onboarding spaces for new users (Synapse): you can get closer to Discord by auto‑joining users into your main spaces on first login/provisioning:
# homeserver.yaml
auto_join_rooms:
  - "#mychannel:matrix.example.com"
  - "#mychannel-community:matrix.example.com"
autocreate_auto_join_rooms: false

Notes:

  • This only affects newly created/provisioned users (it is not applied retroactively to existing users).
  • It auto‑joins the spaces; users can then join suggested rooms with one click (and with restricted joins you can make “membership in the space” the permission to join the rooms).

Extending with Bots (Application Services)

Matrix’s Application Service (AS) API lets you build bots and integrations that run alongside Synapse. Unlike simple webhooks, an appservice receives events from all rooms it’s registered for, can act as a virtual user (e.g., @mybot:example.com), and auto-joins rooms when invited.

Two Approaches

ApproachWhen to Use
Incoming webhookOne-way notifications (alerts, CI/CD updates)
Application ServiceTwo-way bots (commands, bridging, automation)

Building an Application Service

The example below is a minimal chatops bot — a starting point you can extend for real-world use cases:

  • Monitoring alerts: forward alerts from Prometheus/Alertmanager into a Matrix room
  • CI/CD notifications: post build results from GitHub Actions or GitLab CI
  • Helpdesk bridge: create a ticket in Jira/Linear when a user types !ticket
  • ChatOps commands: trigger deployments, query status, or run runbooks from a chat room

The bot reacts to !ping with pong to demonstrate the event loop. Replace handle_event with your own logic.

An appservice is a plain HTTP server. Synapse pushes events to it via PUT /_matrix/app/v1/transactions/{txnId}.

Step 1: Registration file

Tell Synapse your bot exists via a registration YAML (stored as a Kubernetes Secret and mounted into Synapse):

id: "my_bot"
url: "http://my-bot:8080"     # internal Kubernetes service URL
as_token: "<random-secret>"   # Synapse → bot authentication
hs_token: "<random-secret>"   # bot → Synapse authentication
sender_localpart: "my-bot"    # creates @my-bot:example.com
rate_limited: false
namespaces:
  users:
    - exclusive: true
      regex: '@my-bot:example\.com'
  aliases: []
  rooms: []

Reference it in homeserver.yaml:

app_service_config_files:
  - /data/appservices/my-bot-registration.yaml

Step 2: The bot server

import json, os, uuid
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import Request, urlopen
from urllib.parse import quote, urlencode

HS_URL   = os.environ["HS_URL"]     # http://synapse:8008
AS_TOKEN = os.environ["AS_TOKEN"]
HS_TOKEN = os.environ["HS_TOKEN"]
BOT_USER = "@my-bot:example.com"

def send_message(room_id: str, text: str) -> None:
    txn = uuid.uuid4().hex
    url = (f"{HS_URL}/_matrix/client/v3/rooms/{quote(room_id, safe='')}"
           f"/send/m.room.message/{txn}"
           f"?{urlencode({'access_token': AS_TOKEN, 'user_id': BOT_USER})}")
    body = json.dumps({"msgtype": "m.text", "body": text}).encode()
    urlopen(Request(url, method="PUT", data=body,
                    headers={"Content-Type": "application/json"}))

def handle_event(event: dict) -> None:
    if event.get("type") != "m.room.message":
        return
    body = (event.get("content") or {}).get("body", "")
    if body.strip() == "!ping":
        send_message(event["room_id"], "pong")

class Handler(BaseHTTPRequestHandler):
    def do_PUT(self):
        length = int(self.headers.get("Content-Length", 0))
        payload = json.loads(self.rfile.read(length) or "{}")
        for event in payload.get("events", []):
            try:
                handle_event(event)
            except Exception as e:
                print(f"error: {e}")
        raw = b"{}"
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", "2")
        self.end_headers()
        self.wfile.write(raw)

    def log_message(self, *_): pass

HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

Step 3: Deploy Matrix on Kubernetes

Store the bot code in a ConfigMap and mount it into a minimal Python container — no custom image required:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-bot
  namespace: matrix
data:
  bot.py: |
    # ... bot code ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-bot
  namespace: matrix
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-bot
  template:
    metadata:
      labels:
        app: my-bot
    spec:
      containers:
        - name: bot
          image: python:3.13-alpine
          command: ["python", "/app/bot.py"]
          envFrom:
            - secretRef:
                name: my-bot-secrets  # AS_TOKEN, HS_TOKEN, etc.
          volumeMounts:
            - name: app
              mountPath: /app
      volumes:
        - name: app
          configMap:
            name: my-bot
---
apiVersion: v1
kind: Service
metadata:
  name: my-bot
  namespace: matrix
spec:
  selector:
    app: my-bot
  ports:
    - port: 8080

Step 4: Register the bot user

If your server uses MAS (OIDC), you must register the bot user explicitly before the first invite:

curl -X POST "http://synapse:8008/_matrix/client/v3/register?access_token=<as_token>" \
  -H "Content-Type: application/json" \
  -d '{"type": "m.login.application_service", "username": "my-bot", "inhibit_login": true}'

Then invite @my-bot:example.com to a room — the appservice auto-joins on invite.

Key Patterns

  • Use internal URLs (http://synapse:8008) — avoids going through the ingress
  • Store tokens in Kubernetes Secrets, never ConfigMaps
  • Unencrypted rooms only — without an E2EE library, the bot receives m.room.encrypted blobs it can’t read
  • Handle duplicate transactions — Synapse may replay a txnId on reconnect; make event handling idempotent

What You’ve Built

By completing this guide on how to deploy Matrix on Kubernetes, you now have:

  1. Enterprise-grade communication platform

    • Voice, video, screen sharing (LiveKit)
    • OIDC/SSO integration (MAS)
    • High availability (managed DBaaS, load balancers)
  2. GDPR-compliant infrastructure

    • EU data residency (Exoscale CH/DE/AT zones)
    • Full audit trails and access controls
    • No US Cloud Act exposure
  3. Production-ready architecture

    • Automatic backups and disaster recovery
    • TLS encryption (Let’s Encrypt)
    • Scalable to 1,000+ users with minimal adjustments
  4. Cost-optimized stack

    • Managed services reduce operational overhead
    • Object storage (SOS) for media at 90% lower cost
    • Pay only for what you use (no per-user licensing)

Additional Resources

Exoscale Documentation

Matrix Documentation


Ready to deploy? Start with Exoscale SKS and revisit Phase 1 of this guide. For deeper context on data sovereignty requirements, read our companion article: Data Sovereignty on Exoscale.

Questions? Reach out to Exoscale support or join the Matrix community.

LinkedIn Bluesky