Slack, like many organization in the current recession, are tightening their belt. Originally I used the software to chat with my better half. Over the years I have added friends and the older spawn. Definitely hit the integration limit but paying at minimum $25 a month might be above my families budget. So checking out alternatives.

Matrix is a decentralized communication system. Originally they implemented their reference software in Python known as Synapse. Some have been working on another reference implementation in Go named Dendrite.

Dendrite Dependencies

Dendrite does require a database however it may user either SQLite or Postgres. Out of laziness I will start with SQLite then move over to Postgres.

Containers

Looks like Dendrite Monolith has an active image. Looks like developers commonly deploy on via docker-compose so I will need to translate this into Kubernetes deployments.

First Deployment Attempt

Given the state storage via SQLite, a StatefulSet is probably the best bet. Port 8008 appears to be the HTTP port.

apiVersion: v1
kind: Service
metadata:
  name: dendrite
  namespace: xp-dendrite
  labels:
    app: dendrite
spec:
  ports:
    - port: 80
      name: http
      targetPort: 8008
  selector:
    app: dendrite
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: dendrite
  namespace: xp-dendrite
spec:
  selector:
    matchLabels:
      app: dendrite
  serviceName: "dendrite"
  replicas: 1
  template:
    metadata:
      labels:
        app: dendrite
    spec:
      initContainers:
        - name: key-gen
          image: matrixdotorg/dendrite-monolith:latest
          command: ["/usr/bin/generate-keys","-private-key", "/mnt/matrix_key.pem", "-tls-cert","/mnt/server.crt", "-tls-key","/mnt/server.key"]
          volumeMounts:
            - name: storage-keys
              mountPath: /mnt
      containers:
        - name: monolith
          image: matrixdotorg/dendrite-monolith:latest
          args: ["-config", "/etc/dendrite/config.yaml"]
          ports:
            - containerPort: 8008
              name: http
          volumeMounts:
            - name: jetstream
              mountPath: /var/dendrite/jetstream
            - name: sqlite
              mountPath: /var/sqlite/dendrite
            - name: config
              mountPath: /etc/dendrite
              readOnly: true
            - name: media
              mountPath: /var/dendrite/media
            - name: storage-keys
              mountPath: /var/dendrite/storage-keys
              readOnly: true
            - name: logs
              mountPath: /var/dendrite/logs
      volumes:
        - name: config
          configMap:
            name: dendrite
            optional: false
        - name: storage-keys
          emptyDir: {}
  volumeClaimTemplates:
    - metadata:
        name: jetstream
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: "longhorn"
        resources:
          requests:
            storage: 512Mi
    - metadata:
        name: sqlite
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: "longhorn"
        resources:
          requests:
            storage: 512Mi
    - metadata:
        name: media
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: "longhorn"
        resources:
          requests:
            storage: 512Mi
    - metadata:
        name: logs
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: "longhorn"
        resources:
          requests:
            storage: 512Mi

Based on the official monolith configuration file, mine looks like the following:

# This is the Dendrite configuration file.
#
# The configuration is split up into sections - each Dendrite component has a
# configuration section, in addition to the "global" section which applies to
# all components.

# The version of the configuration file.
version: 2

# Global Matrix configuration. This configuration applies to all components.
global:
  # The domain name of this homeserver.
  server_name: xp-dendrite-r0.workshop.meschbach.org

  # The path to the signing private key file, used to sign requests and events.
  # Note that this is NOT the same private key as used for TLS! To generate a
  # signing key, use "./bin/generate-keys --private-key matrix_key.pem".
  private_key: /var/dendrite/storage-keys/matrix_key.pem

  # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
  # to old signing private keys that were formerly in use on this domain. These
  # keys will not be used for federation request or event signing, but will be
  # provided to any other homeserver that asks when trying to verify old events.
  old_private_keys:
  #  - private_key: old_matrix_key.pem
  #    expired_at: 1601024554498

  # How long a remote server can cache our server signing key before requesting it
  # again. Increasing this number will reduce the number of requests made by other
  # servers for our key but increases the period that a compromised key will be
  # considered valid by other homeservers.
  key_validity_period: 168h0m0s

  # Global database connection pool, for PostgreSQL monolith deployments only. If
  # this section is populated then you can omit the "database" blocks in all other
  # sections. For polylith deployments, or monolith deployments using SQLite databases,
  # you must configure the "database" block for each component instead.
  #  database:
  #    connection_string: file:///var/sqlite/dendrite
  #    max_open_conns: 10
  #    max_idle_conns: 2
  #    conn_max_lifetime: -1

  # Configuration for in-memory caches. Caches can often improve performance by
  # keeping frequently accessed items (like events, identifiers etc.) in memory
  # rather than having to read them from the database.
  cache:
    # The estimated maximum size for the global cache in bytes, or in terabytes,
    # gigabytes, megabytes or kilobytes when the appropriate 'tb', 'gb', 'mb' or
    # 'kb' suffix is specified. Note that this is not a hard limit, nor is it a
    # memory limit for the entire process. A cache that is too small may ultimately
    # provide little or no benefit.
    max_size_estimated: 1gb

    # The maximum amount of time that a cache entry can live for in memory before
    # it will be evicted and/or refreshed from the database. Lower values result in
    # easier admission of new cache entries but may also increase database load in
    # comparison to higher values, so adjust conservatively. Higher values may make
    # it harder for new items to make it into the cache, e.g. if new rooms suddenly
    # become popular.
    max_age: 1h

  # The server name to delegate server-server communications to, with optional port
  # e.g. localhost:443
  well_known_server_name: "http://dendrite.xp-dendrite.svc.workshop.k8s"

  # The server name to delegate client-server communications to, with optional port
  # e.g. localhost:443
  well_known_client_name: "http://dendrite.xp-dendrite.svc.workshop.k8s"

  # Lists of domains that the server will trust as identity servers to verify third
  # party identifiers such as phone numbers and email addresses.
  trusted_third_party_id_servers:
  #    - matrix.org
  #    - vector.im

  # Disables federation. Dendrite will not be able to communicate with other servers
  # in the Matrix federation and the federation API will not be exposed.
  disable_federation: true

  # Configures the handling of presence events. Inbound controls whether we receive
  # presence events from other servers, outbound controls whether we send presence
  # events for our local users to other servers.
  presence:
    enable_inbound: false
    enable_outbound: false

  # Configures phone-home statistics reporting. These statistics contain the server
  # name, number of active users and some information on your deployment config.
  # We use this information to understand how Dendrite is being used in the wild.
  report_stats:
    enabled: false
    endpoint: https://matrix.org/report-usage-stats/push

  # Server notices allows server admins to send messages to all users on the server.
  server_notices:
    enabled: false
    # The local part, display name and avatar URL (as a mxc:// URL) for the user that
    # will send the server notices. These are visible to all users on the deployment.
    local_part: "_server"
    display_name: "Server Alerts"
    avatar_url: ""
    # The room name to be used when sending server notices. This room name will
    # appear in user clients.
    room_name: "Server Alerts"

  # Configuration for NATS JetStream
  jetstream:
    # A list of NATS Server addresses to connect to. If none are specified, an
    # internal NATS server will be started automatically when running Dendrite in
    # monolith mode. For polylith deployments, it is required to specify the address
    # of at least one NATS Server node.
    addresses:
    # - localhost:4222

    # Disable the validation of TLS certificates of NATS. This is
    # not recommended in production since it may allow NATS traffic
    # to be sent to an insecure endpoint.
    disable_tls_validation: false

    # Persistent directory to store JetStream streams in. This directory should be
    # preserved across Dendrite restarts.
    storage_path: /var/dendrite/jetstream

    # The prefix to use for stream names for this homeserver - really only useful
    # if you are running more than one Dendrite server on the same NATS deployment.
    topic_prefix: Dendrite

  # Configuration for Prometheus metric collection.
  metrics:
    enabled: false
    basic_auth:
      username: metrics
      password: metrics

  # Optional DNS cache. The DNS cache may reduce the load on DNS servers if there
  # is no local caching resolver available for use.
  dns_cache:
    enabled: false
    cache_size: 256
    cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration

# Configuration for the Appservice API.
app_service_api:
  # Disable the validation of TLS certificates of appservices. This is
  # not recommended in production since it may allow appservice traffic
  # to be sent to an insecure endpoint.
  disable_tls_validation: false
  database:
    connection_string: file:///var/sqlite/dendrite/appservice
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

  # Appservice configuration files to load into this homeserver.
  config_files:
  #  - /path/to/appservice_registration.yaml

# Configuration for the Client API.
client_api:
  # Prevents new users from being able to register on this homeserver, except when
  # using the registration shared secret below.
  registration_disabled: true

  # Prevents new guest accounts from being created. Guest registration is also
  # disabled implicitly by setting 'registration_disabled' above.
  guests_disabled: true

  # If set, allows registration by anyone who knows the shared secret, regardless
  # of whether registration is otherwise disabled.
  registration_shared_secret: ""

  # Whether to require reCAPTCHA for registration. If you have enabled registration
  # then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used
  # for coordinated spam attacks.
  enable_registration_captcha: false

  # Settings for ReCAPTCHA.
  recaptcha_public_key: ""
  recaptcha_private_key: ""
  recaptcha_bypass_secret: ""
  recaptcha_siteverify_api: ""

  # TURN server information that this homeserver should send to clients.
  turn:
    turn_user_lifetime: "5m"
    turn_uris:
    #  - turn:turn.server.org?transport=udp
    #  - turn:turn.server.org?transport=tcp
    turn_shared_secret: ""
    # If your TURN server requires static credentials, then you will need to enter
    # them here instead of supplying a shared secret. Note that these credentials
    # will be visible to clients!
    # turn_username: ""
    # turn_password: ""

  # Settings for rate-limited endpoints. Rate limiting kicks in after the threshold
  # number of "slots" have been taken by requests from a specific host. Each "slot"
  # will be released after the cooloff time in milliseconds. Server administrators
  # and appservice users are exempt from rate limiting by default.
  rate_limiting:
    enabled: true
    threshold: 20
    cooloff_ms: 500
    exempt_user_ids:
    #  - "@user:domain.com"

# Configuration for the Federation API.
federation_api:
  # How many times we will try to resend a failed transaction to a specific server. The
  # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once
  # the max retries are exceeded, Dendrite will no longer try to send transactions to
  # that server until it comes back to life and connects to us again.
  send_max_retries: 16

  # Disable the validation of TLS certificates of remote federated homeservers. Do not
  # enable this option in production as it presents a security risk!
  disable_tls_validation: false

  # Perspective keyservers to use as a backup when direct key fetches fail. This may
  # be required to satisfy key requests for servers that are no longer online when
  # joining some rooms.
  key_perspectives:
    - server_name: matrix.org
      keys:
        - key_id: ed25519:auto
          public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
        - key_id: ed25519:a_RXGa
          public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ

  # This option will control whether Dendrite will prefer to look up keys directly
  # or whether it should try perspective servers first, using direct fetches as a
  # last resort.
  prefer_direct_fetch: false
  database:
    connection_string: file:///var/sqlite/dendrite/federationapi
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

# Configuration for the Media API.
media_api:
  # Storage path for uploaded media. May be relative or absolute.
  base_path: /var/dendrite/media
  database:
    connection_string: file:///var/sqlite/dendrite/mediaapi
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

  # The maximum allowed file size (in bytes) for media uploads to this homeserver
  # (0 = unlimited). If using a reverse proxy, ensure it allows requests at least
  #this large (e.g. the client_max_body_size setting in nginx).
  max_file_size_bytes: 10485760

  # Whether to dynamically generate thumbnails if needed.
  dynamic_thumbnails: false

  # The maximum number of simultaneous thumbnail generators to run.
  max_thumbnail_generators: 10

  # A list of thumbnail sizes to be generated for media content.
  thumbnail_sizes:
    - width: 32
      height: 32
      method: crop
    - width: 96
      height: 96
      method: crop
    - width: 640
      height: 480
      method: scale

# Configuration for enabling experimental MSCs on this homeserver.
mscs:
  mscs:
  #  - msc2836  # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836)
  #  - msc2946  # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946)
  database:
    connection_string: file:///var/sqlite/dendrite/mscs
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

# Configuration for the Sync API.
sync_api:
  # This option controls which HTTP header to inspect to find the real remote IP
  # address of the client. This is likely required if Dendrite is running behind
  # a reverse proxy server.
  # real_ip_header: X-Real-IP
  database:
    connection_string: file:///var/sqlite/dendrite/syncapi
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

# Configuration for the User API.
user_api:
  # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31
  # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information.
  # Setting this lower makes registration/login consume less CPU resources at the cost
  # of security should the database be compromised. Setting this higher makes registration/login
  # consume more CPU resources but makes it harder to brute force password hashes. This value
  # can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds).
  bcrypt_cost: 10

  # The length of time that a token issued for a relying party from
  # /_matrix/client/r0/user/{userId}/openid/request_token endpoint
  # is considered to be valid in milliseconds.
  # The default lifetime is 3600000ms (60 minutes).
  # openid_token_lifetime_ms: 3600000
  account_database:
    connection_string: file:///var/sqlite/dendrite/userapi
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

# Configuration for Opentracing.
# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on
# how this works and how to set it up.
tracing:
  enabled: false
  jaeger:
    serviceName: ""
    disabled: false
    rpc_metrics: false
    tags: []
    sampler: null
    reporter: null
    headers: null
    baggage_restrictions: null
    throttler: null

# Logging configuration. The "std" logging type controls the logs being sent to
# stdout. The "file" logging type controls logs being written to a log folder on
# the disk. Supported log levels are "debug", "info", "warn", "error".
logging:
  - type: std
    level: info

# The following were taken from https://matrix-org.github.io/dendrite/installation/database
# and https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.polylith.yaml
room_server:
  database:
    connection_string: file:///var/sqlite/dendrite/roomserver
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30
key_server:
  database:
    connection_string: file:///var/sqlite/dendrite/keyserver
    max_open_conns: 10
    max_idle_conns: 2
    conn_max_lifetime: 30

Well, definitely not production ready. Nor is there a built-in client which makes sense. At a future point I will have to carve out time to find a decent client to see if it works as a reasonable competitor to Slack.