Servidor Matrix Caseiro: Guia Completo
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 máquina ligada 24/7: PC x86_64 ou SBC (Raspberry Pi 4/5, Odroid N2+, etc.), de preferência com ~8 GB de RAM e disco extra (USB 3.0/SATA).
- Cabo Ethernet, acesso à configuração do roteador, um nobreak se a energia oscilar.
- Fonte oficial na SBC, sobretudo com HD no USB.
- Um computador à parte para SSH e
kubectl. - Endereço público no gateway (IPv4 e/ou IPv6) e um nome DNS — detalhado na seção 2.
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
- Linux e SSH
- LAN, IP público no gateway, DNS (Dynv6 → domínio pago)
- K3s
kubectlno desktop- Página de teste na LAN
- Ingress + certificado HTTPS
- Disco local
- Tuwunel + cliente Matrix — pode parar aqui
- (Opcional) RustFS + PostgreSQL (CloudNativePG)
- (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.
- Raspberry Pi 4 / 5: Fedora no Raspberry Pi ou Raspberry Pi Imager + SSH headless.
- Odroid N2+: wiki Hardkernel, imagens em wiki.odroid.com ou Fedora ARM. Grave o
.imgcom Etcher oudd. - PC: instale o Fedora Server (ou a distro escolhida) de forma usual.
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:
- Reserve um IP LAN estático para o servidor.
- Encaminhe 80/TCP (HTTP) e 443/TCP (HTTPS) da WAN para esse IP.
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.
- IPv4 público: o IP WAN do roteador não pode ser o de CGNAT da operadora (faixas típicas
100.64.0.0/10, ou um IPv4 que não bate com o que um site “qual é meu IP” mostra). Sem IPv4 público, o encaminhamento de portas não recebe tráfego de fora. - IPv6 público: se a operadora entregar prefixo global e o roteador/firewall permitir, um registro AAAA no DNS pode substituir ou complementar o A. Muita rede ainda filtra IPv6 de entrada; teste.
- Sem nenhum dos dois, o restante do guia funciona na LAN, mas certificado automático e Matrix federado pela Internet não.
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.
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:
- A / AAAA no apex (
choppa.xyz) → IPv4 / IPv6 WAN do roteador - CNAME
talk→choppa.xyz(o Traefik distinguetalk.choppa.xyzdo raiz; o IP continua o do gateway)
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.
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).
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
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.
Aviso de site inseguro costuma ser HTTP ou certificado ainda não emitido:
$ 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.
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.
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 |
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 |
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
Login: Telegram (2FA / bot), WhatsApp (QR), Discord (token), Instagram (cookie/QR) — siga a documentação da rede. Conversa privada com o MXID do bot:
Várias redes no mesmo cliente Matrix: