Servidor Matrix Caseiro: Guia Completo

Homeserver header

A “nuvem” é o computador de outra pessoa. Este guia mostra como hospedar em casa um servidor Matrix (chat federado), com HTTPS, disco persistente e, se quiser, pontes para Telegram, WhatsApp, Discord e Instagram.

No fim você terá um homeserver Tuwunel em algo como https://talk.seudominio, acessível por Element ou Nheko. Kubernetes aqui é só o sistema que empacota e reinicia os programas; o chat em si é o Matrix.

Isto não é um instalador de um clique. Exige colar arquivos YAML, SSH e acesso ao roteador.

O que você precisa

Uma caixa só, armazenamento local. Várias máquinas com NFS compartilhado é possível; não está coberto aqui.

Não publique arquivos com senhas. Troque todos os changeme / change-me-*.

Ordem dos passos

  1. Linux e SSH
  2. LAN, IP público no gateway, DNS (Dynv6 → domínio pago)
  3. K3s
  4. kubectl no desktop
  5. Página de teste na LAN
  6. Ingress + certificado HTTPS
  7. Disco local
  8. Tuwunel + cliente Matrix — pode parar aqui
  9. (Opcional) RustFS + PostgreSQL (CloudNativePG)
  10. (Opcional) pontes Mautrix

Conceitos de container e Kubernetes com mais profundidade: cursos da Nana Janashia (Docker) e Kubernetes. Federação: Simply Explained. Visão geral de K3s: documentação oficial.


1. Linux e SSH

O caminho deste texto é Fedora Server. Outra distro atual serve se rodar o script do K3s. Precisa de SSH funcionando.

Sem monitor: grave o cartão, ligue na rede, descubra o IP no painel DHCP do roteador (ou o comando abaixo, com teclado uma vez) e entre. Neste guia, blocos de código com $ no início da linha são o que você digita no terminal; as linhas seguintes, sem $, são a saída típica.

$ ip a
$ ssh usuario@192.168.3.10

No Fedora, depois do primeiro boot:

$ sudo dnf update -y
$ sudo dnf install -y curl tar vim
$ mkdir -p ~/.ssh && chmod 700 ~/.ssh
# Coloque a chave pública do desktop em ~/.ssh/authorized_keys

Deixe o SELinux em enforcing. O K3s convive com ele.


2. Rede: LAN, IP público e DNS

Ethernet, não Wi-Fi, para o servidor ser encontrado sem configurar wireless.

LAN e encaminhamento de portas

No roteador:

  1. Reserve um IP LAN estático para o servidor.
  2. Encaminhe 80/TCP (HTTP) e 443/TCP (HTTPS) da WAN para esse IP.

Reserva de IP estático

Encaminhamento de portas

O IP da máquina (192.168…) vale só dentro de casa. Quem o mundo alcança é o gateway (o roteador): o endereço WAN.

O gateway precisa de um endereço público

Let’s Encrypt (HTTP-01), federação Matrix e qualquer https://seunome exigem que a Internet chegue nas portas 80 e 443 desse gateway.

Confira o IPv4 WAN no painel do roteador e compare com o que ifconfig.co (ou similar) mostra a partir da sua casa. Se forem diferentes, você está atrás de CGNAT: peça IP público à operadora, use um túnel/VPS, ou DNS-01 (não detalhado aqui).

Do DynDNS de teste ao domínio pago

O DNS só aponta um nome para esse IP público (A = IPv4, AAAA = IPv6). O destino não muda quando você troca de provedor de nomes; muda só quem publica o registro.

1. Teste (grátis): Dynv6, No-IP, etc. Você ganha um hostname feio (algo.dynv6.net). Crie um A e/ou AAAA para o IP WAN. Se o IPv4 WAN mudar com o tempo, ative DDNS no roteador ou um cliente na LAN — o Dynv6 e equivalentes esperam isso.

Zona DNS (Dynv6)

Cliente DDNS

Use esse nome no Ingress e no ClusterIssuer enquanto valida HTTPS.

2. Produção (pago): compre um domínio num registrador. Este guia usa Dynadot, que também hospeda a zona DNS. Apontar os mesmos IPs públicos:

DNS Dynadot

Se o IPv4 ainda for dinâmico, o Dynadot (ou Cloudflare, etc.) precisa de atualização DDNS da mesma forma que o Dynv6. IP WAN estático simplifica.

Troca Dynv6 → Dynadot: altere host: e a lista tls.hosts no Ingress (e TUWUNEL_* / well-known) para o domínio novo; o encaminhamento de portas e o IP público não mudam. Espere a propagação (dnschecker.org ou o comando abaixo).

No terminal (desktop ou servidor), o endereço tem de ser o IPv4/IPv6 público do gateway, o mesmo da WAN do roteador:

$ nslookup seudominio.exemplo
Server:         192.168.3.1
Address:        192.168.3.1#53

Name:   seudominio.exemplo
Address: 200.x.x.x

3. Instalar o K3s

K3s é um Kubernetes leve. Um k3s server numa caixa só já é um cluster de um nó. Não instale Ingress-Nginx: o K3s já traz Traefik.

No Fedora, abra o firewall antes de depender da rede dos pods:

$ sudo firewall-cmd --permanent --zone=trusted --add-interface=cni0
$ sudo firewall-cmd --permanent --zone=trusted --add-interface=flannel.1
$ sudo firewall-cmd --permanent --add-port=6443/tcp
$ sudo firewall-cmd --permanent --add-port=80/tcp
$ sudo firewall-cmd --permanent --add-port=443/tcp
$ sudo firewall-cmd --reload

Instalação:

$ curl -sfL https://get.k3s.io | sh -s - server \
    --write-kubeconfig-mode=644 \
    --node-name choppa-server

O script instala k3s, containerd e o serviço systemd. No terminal do servidor:

$ sudo k3s kubectl get nodes
NAME            STATUS   ROLES                  AGE   VERSION
choppa-server   Ready    control-plane,master   1m    v1.31.x+k3s1

A coluna STATUS precisa ser Ready. Se não ficar Ready em SBC, habilite cgroups v2 e memory (cgroup_memory=1 cgroup_enable=memory em /boot/firmware/cmdline.txt ou /boot/cmdline.txt e reboot). Confira no terminal:

$ grep cgroup /proc/mounts
cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime 0 0
$ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
...
memory          0               51              1

(cgroup2 montado; memory com 1 na última coluna.) No servidor, kubectl chama k3s kubectl.


4. kubectl no desktop

Instale kubectl no PC.

No servidor:

$ sudo k3s kubectl config view --raw

O mesmo conteúdo está em /etc/rancher/k3s/k3s.yaml. Copie para ~/.kube/config no desktop. Troque 127.0.0.1 em server: pelo IP LAN do servidor.

No terminal do desktop:

$ kubectl get nodes
NAME            STATUS   ROLES                  AGE   VERSION
choppa-server   Ready    control-plane,master   9d    v1.31.x+k3s1

Se a saída for connection refused em localhost:8080, o campo server: do kubeconfig ainda aponta para a máquina local.

Interface gráfica opcional: Lens / OpenLens / Freelens, colando o mesmo kubeconfig.

Lens

Recursos no Lens

Crie o namespace dos serviços deste guia:

$ kubectl create namespace choppa
namespace/choppa created

5. Página de teste na LAN

Pod = um (ou mais) container em execução. Deployment = “quero N cópias desse pod”. Service = nome interno e porta estável. ConfigMap = arquivo de configuração. O YAML descreve o estado desejado (idempotência): reaplicar o mesmo arquivo não recria o que já está certo.

Salve como welcome.yaml. A imagem ancapepe/http-server é para ARM. Em PC x86_64, use uma imagem multi-arch equivalente ou teste só com kubectl get pods depois.

apiVersion: v1                        # Versão da API
kind: ConfigMap                       # Dados estáticos (arquivo de texto)
metadata:
  name: welcome-config
  namespace: choppa
data:
  welcome.txt: |
    Hello, world!
---
apiVersion: apps/v1
kind: Deployment                      # Controla réplicas do pod
metadata:
  name: welcome-deploy
  namespace: choppa
  labels:
    app: welcome                      # Rótulo para o Service achar os pods
spec:
  replicas: 1
  selector:
    matchLabels:
      app: welcome
  template:
    metadata:
      labels:
        app: welcome
    spec:
      containers:
      - name: welcome
        image: ancapepe/http-server:python-latest
        ports:
        - containerPort: 8000
          name: welcome-http
        volumeMounts:
          - name: welcome-volume
            mountPath: /serve/welcome.txt
            subPath: welcome.txt
      volumes:
      - name: welcome-volume
        configMap:
          name: welcome-config
---
apiVersion: v1
kind: Service                         # Porta interna do cluster
metadata:
  name: welcome-service
  namespace: choppa
spec:
  type: LoadBalancer                  # Também escuta no IP da máquina (LAN)
  selector:
    app: welcome
  ports:
    - protocol: TCP
      port: 8080
      targetPort: welcome-http

No terminal do desktop:

$ kubectl apply -f welcome.yaml
configmap/welcome-config created
deployment.apps/welcome-deploy created
service/welcome-service created
$ kubectl get pods -n choppa
NAME                             READY   STATUS    RESTARTS   AGE
welcome-deploy-5bd4cb78b-hvj9z   1/1     Running   0          75s

READY deve ser 1/1 e STATUS Running. No navegador da LAN: http://IP-DO-SERVIDOR:8080 (force HTTP).

Página de teste

Pods ganham um sufixo aleatório no nome. Sem -n choppa, o kubectl usa o namespace default.

Se o pod ficar Pending ou CrashLoopBackOff:

$ kubectl describe pod -n choppa welcome-deploy-5bd4cb78b-hvj9z
$ kubectl logs -n choppa welcome-deploy-5bd4cb78b-hvj9z

6. Ingress e certificado HTTPS

O Kubernetes não publica pods na Internet por padrão (um banco de dados não deve ficar aberto). Vários sites compartilham as portas 80 e 443; um reverse proxy escolhe o destino pelo hostname. Em K8s isso é um Ingress. No K3s o controller é o Traefik (ingressClassName: traefik).

Com Ingress, o Service de teste pode ser ClusterIP (remova type: LoadBalancer).

Instale o cert-manager v1.21.1 (não use um latest solto):

$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml
namespace/cert-manager created
...
$ kubectl get pods -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-xxxx                          1/1     Running   0          45s
cert-manager-cainjector-xxxx               1/1     Running   0          45s
cert-manager-webhook-xxxx                  1/1     Running   0          45s

Pods cert-manager

ClusterIssuer pede o certificado à Let’s Encrypt (desafio ACME HTTP-01: o mundo precisa alcançar a porta 80 no seu domínio):

apiVersion: cert-manager.io/v1
kind: ClusterIssuer                 # Emissor válido em qualquer namespace
metadata:
  name: letsencrypt
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <seu-email>
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - http01:
        ingress:
          ingressClassName: traefik   # Mesmo controller do Ingress (1.12+)

Aplique. Depois o Ingress com TLS (um arquivo só, por exemplo ingress.yaml):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: proxy
  namespace: choppa
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: traefik
  tls:
  - hosts:
      - seudominio.exemplo
    secretName: certificate           # O cert-manager grava o certificado aqui
  rules:
  - host: seudominio.exemplo
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: welcome-service
              port:
                number: 8080

No terminal:

$ kubectl get secret certificate -n choppa
NAME          TYPE                DATA   AGE
certificate   kubernetes.io/tls   2      2m

O navegador deve mostrar o cadeado em https://seudominio.exemplo.

Secret do certificado

Acesso HTTPS

Aviso de site inseguro costuma ser HTTP ou certificado ainda não emitido:

Aviso do navegador

$ curl https://seudominio.exemplo/welcome.txt
Hello, world!

Se o certificado não sair: CGNAT, porta 80 fechada, DNS ainda não apontando, ou Traefik sem receber o desafio. Documentação ACME: cert-manager.io.


7. Disco persistente

Pods são recriados. Dados que precisam sobreviver vão para um Persistent Volume, pedido por um PersistentVolumeClaim. Uma StorageClass cria o volume automaticamente.

Camadas de storage

Usamos o local-path-provisioner com nome próprio (homelab.local/storage-local), para não conflitar com a classe local-path que o K3s já instala, e duas classes (disco grande / pequeno).

Monte os discos no boot:

$ lsblk -f
$ sudo mkdir -p /storage_1tb /storage_500gb
$ echo 'UUID=<uuid-do-1tb> /storage_1tb ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab

No Fedora:

$ sudo restorecon -Rv /storage_1tb /storage_500gb

Aplique storage-local-rbac.yaml e em seguida storage-local.yaml (abaixo). WaitForFirstConsumer faz o volume nascer só quando um pod é agendado, com afinidade ao nó.

apiVersion: v1
kind: Namespace
metadata:
  name: storage-local
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: storage-local-provisioner-service-account
  namespace: storage-local
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: storage-local-provisioner-role
rules:
  - apiGroups: [""]
    resources: ["nodes", "persistentvolumeclaims", "configmaps", "pods", "pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "patch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: storage-local-provisioner-bind
subjects:
  - kind: ServiceAccount
    name: storage-local-provisioner-service-account
    namespace: storage-local
roleRef:
  kind: ClusterRole
  name: storage-local-provisioner-role
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: storage-local-provisioner-role
  namespace: storage-local
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch", "create", "patch", "update", "delete"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: storage-local-provisioner-bind
  namespace: storage-local
subjects:
  - kind: ServiceAccount
    name: storage-local-provisioner-service-account
    namespace: storage-local
roleRef:
  kind: Role
  name: storage-local-provisioner-role
  apiGroup: rbac.authorization.k8s.io
apiVersion: v1
kind: ConfigMap
metadata:
  name: storage-local-config
  namespace: storage-local
data:
  config.json: |-
    {
      "nodePathMap": [],
      "storageClassConfigs": {
        "storage-local-big": {
          "nodePathMap": [
            {
              "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
              "paths": ["/storage_1tb"]
            }
          ]
        },
        "storage-local-small": {
          "nodePathMap": [
            {
              "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
              "paths": ["/storage_500gb"]
            }
          ]
        }
      }
    }
  setup: |-
    #!/bin/sh
    set -eu
    mkdir -m 0777 -p "$VOL_DIR"
  teardown: |-
    #!/bin/sh
    set -eu
    rm -rf "$VOL_DIR"
  helperPod.yaml: |-
    apiVersion: v1
    kind: Pod
    metadata:
      name: helper-pod
    spec:
      priorityClassName: system-node-critical
      containers:
        - name: helper-pod
          image: rancher/mirrored-library-busybox:1.37.0
          imagePullPolicy: IfNotPresent
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: storage-local-provisioner
  namespace: storage-local
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: storage-local-provisioner
  template:
    metadata:
      labels:
        app: storage-local-provisioner
    spec:
      serviceAccountName: storage-local-provisioner-service-account
      containers:
        - name: storage-local-provisioner
          image: rancher/local-path-provisioner:v0.0.36
          command:
            - local-path-provisioner
            - start
            - --config
            - /etc/config/config.json
            - --configmap-name
            - storage-local-config
            - --service-account-name
            - storage-local-provisioner-service-account
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config/
          env:
            - name: PROVISIONER_NAME
              value: homelab.local/storage-local
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: CONFIG_MOUNT_PATH
              value: /etc/config/
      volumes:
        - name: config-volume
          configMap:
            name: storage-local-config
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: storage-local-big
  annotations:
    defaultVolumeType: local
provisioner: homelab.local/storage-local
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: storage-local-small
  annotations:
    defaultVolumeType: local
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: homelab.local/storage-local
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

Teste: PVC + pod que cria o arquivo SUCCESS.

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim-small
  namespace: choppa
spec:
  storageClassName: storage-local-small
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Mi
---
kind: Pod
apiVersion: v1
metadata:
  name: test-pod-small
  namespace: choppa
spec:
  containers:
  - name: test-pod-small
    image: busybox:stable
    command: ["/bin/sh", "-c", "touch /mnt/SUCCESS && exit 0 || exit 1"]
    volumeMounts:
      - name: test-volume
        mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
    - name: test-volume
      persistentVolumeClaim:
        claimName: test-claim-small

No terminal:

$ kubectl apply -f test-storage.yaml
persistentvolumeclaim/test-claim-small created
pod/test-pod-small created
$ kubectl get pod test-pod-small -n choppa
NAME             READY   STATUS      RESTARTS   AGE
test-pod-small   0/1     Completed   0          8s

No servidor, o arquivo SUCCESS deve aparecer sob /storage_500gb no diretório que o provisioner criou.


8. Servidor Matrix (Tuwunel)

Matrix é um protocolo federado: seu servidor conversa com outros (como e-mail), sem uma empresa no meio. Implementações conhecidas: Synapse, Dendrite. Este guia usa Tuwunel (ghcr.io/matrix-construct/tuwunel), sucessor do Conduit (não inicie instâncias novas no Conduit). Dados no RocksDB: uma réplica, estratégia Recreate.

Crie um ConfigMap vazio para pontes futuras:

$ kubectl create configmap matrix-appservices -n choppa --from-literal=placeholder=1
configmap/matrix-appservices created

TUWUNEL_SERVER_NAME é o que aparece no MXID (@usuario:choppa.xyz). Well-known usa dois underscores: TUWUNEL_WELL_KNOWN__CLIENT / __SERVER.

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: matrix-pv-claim
  namespace: choppa
  labels:
    app: matrix
spec:
  storageClassName: storage-local-big
  accessModes:
    - ReadWriteOnce                   # Um escritor; evita corromper o RocksDB
  resources:
    requests:
      storage: 200Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: matrix-config
  namespace: choppa
  labels:
    app: matrix
data:
  TUWUNEL_SERVER_NAME: 'choppa.xyz'
  TUWUNEL_DATABASE_PATH: '/var/lib/matrix-db/'
  TUWUNEL_ADDRESS: '0.0.0.0'
  TUWUNEL_PORT: '6167'
  TUWUNEL_MAX_REQUEST_SIZE: '100000000'
  TUWUNEL_ALLOW_REGISTRATION: 'true'
  TUWUNEL_ALLOW_FEDERATION: 'true'
  TUWUNEL_TRUSTED_SERVERS: '["matrix.org"]'
  TUWUNEL_WELL_KNOWN__CLIENT: 'https://talk.choppa.xyz'
  TUWUNEL_WELL_KNOWN__SERVER: 'talk.choppa.xyz:443'
  TUWUNEL_APPSERVICE_DIR: '/config/appservices/'
  TUWUNEL_DATABASE_BACKUP_PATH: '/var/lib/matrix-backups/'
  TUWUNEL_DATABASE_BACKUPS_TO_KEEP: '2'
---
apiVersion: v1
kind: Secret
metadata:
  name: matrix-secret
  namespace: choppa
type: Opaque
stringData:
  TUWUNEL_REGISTRATION_TOKEN: changeme
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: matrix-deploy
  namespace: choppa
spec:
  replicas: 1
  strategy:
    type: Recreate                    # Derruba o pod antigo antes de subir o novo
  selector:
    matchLabels:
      app: matrix
  template:
    metadata:
      labels:
        app: matrix
    spec:
      enableServiceLinks: false
      containers:
        - name: matrix
          image: ghcr.io/matrix-construct/tuwunel:v1.9.4
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 6167
              name: matrix-port
          envFrom:
            - configMapRef:
                name: matrix-config
          env:
            - name: TUWUNEL_REGISTRATION_TOKEN
              valueFrom:
                secretKeyRef:
                  name: matrix-secret
                  key: TUWUNEL_REGISTRATION_TOKEN
          volumeMounts:
            - name: matrix-storage
              mountPath: /var/lib/matrix-db/
              subPath: db
            - name: matrix-storage
              mountPath: /var/lib/matrix-backups/
              subPath: backups
            - name: matrix-appservices-dir
              mountPath: /config/appservices/
      volumes:
        - name: matrix-storage
          persistentVolumeClaim:
            claimName: matrix-pv-claim
        - name: matrix-appservices-dir
          configMap:
            name: matrix-appservices
---
apiVersion: v1
kind: Service
metadata:
  name: matrix-service
  namespace: choppa
spec:
  selector:
    app: matrix
  ports:
    - name: federation
      protocol: TCP
      port: 8448
      targetPort: matrix-port

Com o DNS da seção 2 já apontando o apex e o CNAME talk para o IP público do gateway, atualize o Ingress: tls.hosts, /.well-known/matrix/ no domínio raiz e / em talk.:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: proxy
  namespace: choppa
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: traefik
  tls:
  - hosts:
      - choppa.xyz
      - talk.choppa.xyz
    secretName: certificate
  rules:
  - host: choppa.xyz
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: welcome-service
              port:
                number: 8080
        - path: /.well-known/matrix/
          pathType: ImplementationSpecific
          backend:
            service:
              name: matrix-service
              port:
                number: 8448
  - host: talk.choppa.xyz
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: matrix-service
              port:
                number: 8448

Se TUWUNEL_SERVER_NAME for diferente do host HTTP, outros servidores Matrix usam well-known.

No terminal:

$ kubectl get pods -n choppa
NAME                             READY   STATUS    RESTARTS   AGE
matrix-deploy-xxxxxxxxxx-xxxxx   1/1     Running   0          2m

No cliente Matrix (com suporte a token de registro): homeserver https://talk.choppa.xyz e o token do Secret.

Pod do homeserver

Login Element

Quem só quer Matrix pode parar neste ponto. Tuwunel não usa o PostgreSQL abaixo; o Postgres é para as pontes.


9. PostgreSQL e backup (opcional)

As pontes Mautrix gravam estado em Postgres. Use o operador CloudNativePG (instances: 1 neste guia). Backups: RustFS (API S3; no cluster os objetos ainda se chamam minio-*) + plugin Barman Cloud.

$ kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml
$ kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/latest/download/manifest.yaml
$ kubectl get pods -n cnpg-system
NAME                                     READY   STATUS    RESTARTS   AGE
cnpg-controller-manager-xxxx             1/1     Running   0          1m

Object storage (RustFS)

Crie o bucket postgres-backups no console (porta 9001) e ligue versionamento antes de depender do backup.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minio-backup
  namespace: choppa
spec:
  storageClassName: storage-local-big
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 50Gi
---
apiVersion: v1
kind: Secret
metadata:
  name: minio-secret
  namespace: choppa
type: Opaque
stringData:
  RUSTFS_ROOT_USER: admin
  RUSTFS_ROOT_PASSWORD: change-me-backup
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio-deploy
  namespace: choppa
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
        - name: minio
          image: rustfs/rustfs:1.0.0
          args: ["server", "/data", "--console-address", ":9001"]
          envFrom:
            - secretRef:
                name: minio-secret
          ports:
            - name: minio-api-port
              containerPort: 9000
            - name: minio-ui-port
              containerPort: 9001
          volumeMounts:
            - mountPath: /data
              name: minio-data
      volumes:
        - name: minio-data
          persistentVolumeClaim:
            claimName: minio-backup
---
apiVersion: v1
kind: Service
metadata:
  name: minio-service
  namespace: choppa
spec:
  selector:
    app: minio
  ports:
    - name: minio-api
      port: 9000
      targetPort: minio-api-port
---
apiVersion: v1
kind: Service
metadata:
  name: minio-external-service
  namespace: choppa
spec:
  type: LoadBalancer
  selector:
    app: minio
  ports:
    - name: minio-ui
      port: 9001
      targetPort: minio-ui-port

Abra http://IP-DO-SERVIDOR:9001, entre, crie o bucket.

Cluster CloudNativePG

Apps no cluster usam o Service postgres-cluster-rw. Cliente no desktop (ex.: SQLectron): LoadBalancer na porta 5432.

apiVersion: v1
kind: Secret
metadata:
  name: postgres-admin-secret
  namespace: choppa
type: kubernetes.io/basic-auth
stringData:
  username: admin
  password: change-me-app
---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-superuser-secret
  namespace: choppa
type: kubernetes.io/basic-auth
stringData:
  username: postgres
  password: change-me-super
---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-backup-secret
  namespace: choppa
type: Opaque
stringData:
  ACCESS_KEY_ID: admin
  ACCESS_SECRET_KEY: change-me-backup
---
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: postgres-minio-store
  namespace: choppa
spec:
  retentionPolicy: "14d"
  instanceSidecarConfiguration:
    env:
      - name: AWS_REQUEST_CHECKSUM_CALCULATION
        value: when_required
      - name: AWS_RESPONSE_CHECKSUM_VALIDATION
        value: when_required
  configuration:
    destinationPath: s3://postgres-backups/
    endpointURL: http://minio-service:9000
    s3Credentials:
      accessKeyId:
        name: postgres-backup-secret
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: postgres-backup-secret
        key: ACCESS_SECRET_KEY
    wal:
      compression: gzip
    data:
      compression: gzip
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-cluster
  namespace: choppa
spec:
  instances: 1
  imageName: ghcr.io/cloudnative-pg/postgresql:18.1
  enableSuperuserAccess: true
  storage:
    size: 50Gi
    storageClass: storage-local-small
  resources:
    limits:
      memory: 1Gi
      cpu: 500m
  superuserSecret:
    name: postgres-superuser-secret
  bootstrap:
    initdb:
      database: choppadb
      owner: admin
      secret:
        name: postgres-admin-secret
  plugins:
    - name: barman-cloud.cloudnative-pg.io
      isWALArchiver: true
      parameters:
        barmanObjectName: postgres-minio-store
  postgresql:
    parameters:
      max_connections: "100"
      shared_buffers: "256MB"
      wal_compression: "on"
---
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: postgres-backup
  namespace: choppa
spec:
  schedule: "0 0 2 * * *"
  backupOwnerReference: self
  cluster:
    name: postgres-cluster
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-external-service
  namespace: choppa
spec:
  type: LoadBalancer
  selector:
    cnpg.io/cluster: postgres-cluster
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
$ kubectl get pods -n choppa -l cnpg.io/cluster=postgres-cluster
NAME                 READY   STATUS    RESTARTS   AGE
postgres-cluster-1   1/1     Running   0          2m

Por ponte, um database e um usuário (nomes iguais aos da URI):

CREATE DATABASE matrix_telegram;
CREATE USER telegram WITH PASSWORD 'mautrix';
GRANT ALL ON DATABASE matrix_telegram TO telegram;
\c matrix_telegram
GRANT ALL ON SCHEMA public TO telegram;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO telegram;

Repita para whatsapp, discord, instagram.


10. Pontes Mautrix (opcional)

Documentação: docs.mau.fi/bridges. Gere config.yaml e registration.yaml com Docker ou Podman no desktop (instale um deles se ainda não tiver):

$ docker run --rm -v "$PWD":/data:z dock.mau.dev/mautrix/telegram:latest
# Edite config.yaml e rode o mesmo comando de novo para gerar registration.yaml
Ponte Imagem Porta Bot Host do Postgres
Telegram dock.mau.dev/mautrix/telegram:latest 29317 @telegrambot:choppa.xyz postgres-cluster-rw
WhatsApp dock.mau.dev/mautrix/whatsapp:latest 29318 @whatsappbot:choppa.xyz o mesmo
Discord dock.mau.dev/mautrix/discord:latest 29334 @discordbot:choppa.xyz o mesmo
Instagram dock.mau.dev/mautrix/meta:latest 29330 @instagrambot:choppa.xyz o mesmo

Homeserver interno: http://matrix-service:8448. Domínio: choppa.xyz. Appservice: http://<ponte>-service:<porta>.

Copie cada registration.yaml para o ConfigMap matrix-appservices (chaves telegram-registration.yaml, etc.), montado em /config/appservices/. Reinicie o Tuwunel se necessário.

Esqueleto (Telegram; as outras mudam imagem, porta e ConfigMap):

apiVersion: v1
kind: ConfigMap
metadata:
  name: telegram-config
  namespace: choppa
data:
  config.yaml: |
    homeserver:
      address: http://matrix-service:8448
      domain: choppa.xyz
    appservice:
      address: http://telegram-service:29317
      hostname: 0.0.0.0
      port: 29317
      database: postgres://telegram:mautrix@postgres-cluster-rw/matrix_telegram?sslmode=disable
      as_token: <igual ao registration>
      hs_token: <igual ao registration>
    telegram:
      api_id: <https://my.telegram.org/apps>
      api_hash: <gerado no site>
    bridge:
      permissions:
        "*": relay
        "@voce:choppa.xyz": admin
  registration.yaml: |
    id: telegram
    url: http://telegram-service:29317
    as_token: <igual>
    hs_token: <igual>
    sender_localpart: <gerado>
    rate_limited: false
    namespaces:
      users:
        - exclusive: true
          regex: '@telegram_.*:choppa\.xyz'
        - exclusive: true
          regex: '@telegrambot:choppa\.xyz'
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: telegram-deploy
  namespace: choppa
spec:
  replicas: 1
  selector:
    matchLabels:
      app: telegram
  template:
    metadata:
      labels:
        app: telegram
    spec:
      containers:
        - name: telegram
          image: dock.mau.dev/mautrix/telegram:latest
          command: ["python3", "-m", "mautrix_telegram", "-c", "/data/config.yaml", "-r", "/data/registration.yaml", "--no-update"]
          ports:
            - containerPort: 29317
              name: telegram-port
          volumeMounts:
            - name: telegram-volume
              mountPath: /data/config.yaml
              subPath: config.yaml
            - name: telegram-volume
              mountPath: /data/registration.yaml
              subPath: registration.yaml
      volumes:
        - name: telegram-volume
          configMap:
            name: telegram-config
---
apiVersion: v1
kind: Service
metadata:
  name: telegram-service
  namespace: choppa
spec:
  publishNotReadyAddresses: true
  selector:
    app: telegram
  ports:
    - protocol: TCP
      port: 29317
      targetPort: telegram-port
$ kubectl get pods -n choppa -l app=telegram
NAME                              READY   STATUS    RESTARTS   AGE
telegram-deploy-xxxxxxxxxx-xxxxx  1/1     Running   0          1m

Se STATUS for CrashLoopBackOff (usuário/database Postgres inexistente é o caso mais comum):

$ kubectl logs -n choppa deploy/telegram-deploy

Falha da ponte

Login: Telegram (2FA / bot), WhatsApp (QR), Discord (token), Instagram (cookie/QR) — siga a documentação da rede. Conversa privada com o MXID do bot:

Login do bot Telegram

Várias redes no mesmo cliente Matrix:

Nheko