1. Introduccion
Matrix es un protocolo abierto y descentralizado de mensajeria en tiempo real. Synapse es la implementacion de referencia del homeserver (el servidor que aloja las cuentas, las salas y los mensajes), y Element es el cliente mas usado para hablar con el. Esta guia levanta Synapse, su base de datos PostgreSQL, Element Web y un proxy inverso Nginx con Docker, todo autoalojado, sin federar con el resto de la red Matrix y sin depender de ningun proveedor externo.
localhost. Una IP normal de la red servida por http no cumple esa condicion, y sin ella el navegador deshabilita el cifrado que Element necesita. La solucion correcta, la que se usaria en un servidor real, no es un tunel ni un ajuste del navegador: es poner un proxy inverso con HTTPS delante, aunque sea con un certificado autofirmado mientras no haya un dominio publico. Esta guia lo hace con Nginx y un certificado generado con openssl, y es la razon por la que el acceso final es directo por IP, sin trucos del lado del cliente.2. Variables de esta guia
Todos los bloques de configuracion usan los siguientes marcadores en MAYUSCULAS. Sustituyelos por los valores reales del entorno donde se aplique esta guia.
| Marcador | Que es | Donde se obtiene |
|---|---|---|
IP_HOST_DOCKER | IP del host Linux donde corre el stack con Docker | La interfaz de red de ese host |
SERVER_NAME_MATRIX | Nombre de dominio interno del homeserver, forma parte del identificador de cada usuario (@usuario:SERVER_NAME_MATRIX) | Lo defines tu, por ejemplo clockwork.local; no hace falta que resuelva por DNS porque no vamos a federar |
POSTGRES_PASSWORD | ContraseƱa del usuario de la base de datos de Synapse | Se define en /docker/synapse/.env |
USUARIO_MATRIX / PASSWORD_MATRIX | La primera cuenta local que se crea en el homeserver | Los eliges tu al ejecutar el comando de alta de usuario (seccion 9) |
IP_HOST_DOCKER tiene un papel adicional en esta guia: es literalmente el texto que se escribe como CN y subjectAltName al generar el certificado (seccion 7). Si esa IP cambia mas adelante, hay que regenerar el certificado para que siga siendo valido para la nueva direccion.3. Arquitectura y flujo
Todo el trafico del navegador pasa por un unico punto de entrada, el proxy inverso, que reparte segun la ruta: la interfaz de Element por un lado, la API de Synapse por otro, ambas bajo el mismo origen HTTPS — asi no hay ni contenido mixto ni problemas de CORS entre las dos piezas.
[Windows / cualquier navegador o Element Desktop]
|
| https://IP_HOST_DOCKER:8443 (certificado autofirmado)
v
[proxy] :8443 (Nginx, proxy inverso HTTPS)
| \
| \ /_matrix/*, /_synapse/*
v v
[element] :80 [synapse] :8008
|
v
[synapse-db] (PostgreSQL)
4. Requisitos
- Un host Linux con Docker Engine y el plugin Docker Compose (
docker compose, sin guion) ya instalados. - No hace falta salida a Internet desde ese host: todo el flujo de login vive dentro de la red local, sin necesidad de contactar servidores externos.
- Un navegador cualquiera (Chrome, Firefox, Edge...) en cualquier equipo de la misma red, o la app Element Desktop si se prefiere esa via (seccion 11).
5. Levantar Postgres, Synapse y Element con Docker
Misma organizacion de siempre: cada stack en su carpeta dentro de /docker, credenciales en un .env aparte.
sudo mkdir -p /docker/synapse && cd /docker/synapse sudo mkdir -p synapse-data
sudo nano .env
POSTGRES_USER=synapse POSTGRES_PASSWORD=synapse_pwd POSTGRES_DB=synapse
Ahora crea el archivo de Element:
sudo nano element-config.json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://IP_HOST_DOCKER:8443",
"server_name": "SERVER_NAME_MATRIX"
}
},
"brand": "Clockwork Computer Chat"
}
base_url es la direccion real a la que Element se conecta (ahi si va la IP), pero server_name no es una direccion de red, es la segunda mitad del identificador de cada usuario, y queda fijado para siempre en cuanto se genere homeserver.yaml en el paso 6. Por eso conviene usar algo inventado como clockwork.local en vez de la IP para ese campo.Y ahora el docker-compose.yml:
sudo nano docker-compose.yml
services:
synapse-db:
image: postgres:16-alpine
container_name: synapse-db
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
volumes:
- synapse-db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 20
start_period: 20s
restart: unless-stopped
synapse:
image: matrixdotorg/synapse:latest
container_name: synapse
volumes:
- ./synapse-data:/data
ports:
- "8008:8008"
depends_on:
synapse-db:
condition: service_healthy
restart: unless-stopped
element:
image: vectorim/element-web:latest
container_name: element
volumes:
- ./element-config.json:/app/config.json:ro
depends_on:
- synapse
restart: unless-stopped
proxy:
image: nginx:alpine
container_name: proxy
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./proxy-selfsigned.crt:/etc/nginx/certs/proxy-selfsigned.crt:ro
- ./proxy-selfsigned.key:/etc/nginx/certs/proxy-selfsigned.key:ro
ports:
- "8443:8443"
depends_on:
- synapse
- element
restart: unless-stopped
volumes:
synapse-db-data:
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" no es opcional, es la trampa mas repetida al montar Synapse: si la base de datos se crea con el locale por defecto de la imagen, Synapse se niega a arrancar con un error de "Database has incorrect collation... Should be 'C'". Solo se aplica en la primera inicializacion del volumen; si ya arrancaste una vez sin este flag, hay que borrar el volumen y empezar de cero.element no publica ningun puerto al host: solo es alcanzable a traves de proxy, por dentro de la red de Docker. Es proxy el unico punto de entrada desde fuera.6. Generar la configuracion de Synapse
Synapse necesita un homeserver.yaml antes de arrancar de verdad. Se genera con un comando de un solo uso, usando el propio servicio ya definido en el docker-compose.yml:
cd /docker/synapse docker compose run --rm -e SYNAPSE_SERVER_NAME=SERVER_NAME_MATRIX -e SYNAPSE_REPORT_STATS=no synapse generate
Esto crea ./synapse-data/homeserver.yaml y una clave de firma, ya con un registration_shared_secret aleatorio incluido — no hay que generarlo ni copiarlo a mano, el comando de alta de usuarios del paso 9 lo lee directamente del propio archivo.
Edita el archivo generado:
sudo nano ./synapse-data/homeserver.yaml
Busca el bloque database: (por defecto viene configurado para SQLite) y sustituyelo entero por este, apuntando a Postgres:
database:
name: psycopg2
args:
user: synapse
password: synapse_pwd
database: synapse
host: synapse-db
port: 5432
cp_min: 5
cp_max: 10
host: synapse-db es el nombre del servicio en docker-compose.yml, no localhost. Dentro de la red de Compose, los contenedores se hablan por nombre de servicio, nunca por localhost. Si dejaste user y password con otros valores en el .env del paso 5, aqui deben coincidir exactamente.7. Configurar el proxy inverso con HTTPS propio
Primero, genera un certificado autofirmado con clave EC (curva eliptica), valido para la IP del host Docker:
cd /docker/synapse sudo openssl ecparam -name prime256v1 -genkey -noout -out proxy-selfsigned.key sudo openssl req -new -x509 -key proxy-selfsigned.key -out proxy-selfsigned.crt -days 825 \ -subj "/CN=IP_HOST_DOCKER" \ -addext "subjectAltName=IP:IP_HOST_DOCKER"
Ahora crea la configuracion de Nginx:
sudo nano nginx.conf
resolver 127.0.0.11 valid=10s;
server {
listen 8443 ssl;
server_name IP_HOST_DOCKER;
ssl_certificate /etc/nginx/certs/proxy-selfsigned.crt;
ssl_certificate_key /etc/nginx/certs/proxy-selfsigned.key;
location ~ ^/(_matrix|_synapse) {
set $upstream_synapse http://synapse:8008;
proxy_pass $upstream_synapse;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
set $upstream_element http://element:80;
proxy_pass $upstream_element;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
resolver 127.0.0.11 (el DNS interno de Docker) combinado con set $upstream_... ; proxy_pass $upstream_...; es imprescindible, no un capricho de estilo: sin la variable, Nginx intenta resolver synapse y element una sola vez al arrancar, con el resolvedor normal del sistema, y si en ese instante no lo consigue se niega a arrancar con un error de host not found in upstream. Con la variable, la resolucion se hace bajo demanda en cada peticion, tolerando que el contenedor tarde en estar listo o se recree mas adelante con otra IP interna.8. Arrancar el stack y verificar Synapse
cd /docker/synapse docker compose up -d docker compose ps
Sigue el log del homeserver:
docker logs -f synapse
Busca una linea similar a "Synapse now listening on TCP port 8008", sin errores de collation ni excepciones de Python por el camino. Sal con Ctrl+C.
Comprueba tambien que el proxy responde correctamente antes de pasar al navegador:
curl -kv https://IP_HOST_DOCKER:8443/_matrix/client/versions
9. Crear los usuarios
Las cuentas en Synapse se crean con un comando de administracion, ejecutado dentro del propio contenedor:
docker exec -it synapse register_new_matrix_user \ http://localhost:8008 \ -c /data/homeserver.yaml \ -u USUARIO_MATRIX \ -p PASSWORD_MATRIX \ -a
El flag -a marca la cuenta como administradora del homeserver.
enable_registration en su valor por defecto (false): nadie puede registrarse solo desde el cliente, todas las cuentas se crean con este comando por un administrador.Para crear una segunda cuenta (por ejemplo, una que no sea administradora), se repite el mismo comando cambiando el usuario y la contraseƱa, sustituyendo -a por --no-admin:
docker exec -it synapse register_new_matrix_user \ http://localhost:8008 \ -c /data/homeserver.yaml \ -u USUARIO2_MATRIX \ -p PASSWORD2_MATRIX \ --no-admin
11. Alternativa: Element Desktop
Si se prefiere no lidiar con el aviso de certificado, tambien se puede instalar Element Desktop (element.io/download) y conectarla directamente al puerto de Synapse sin pasar por el proxy, ya que al ser una aplicacion nativa no esta sujeta al requisito de contexto seguro del navegador:
http://IP_HOST_DOCKER:8008
Ambas vias llevan al mismo homeserver y a las mismas cuentas; son formas distintas de conectarse, no montajes separados.
12. Tabla de puertos y componentes
| Puerto | Componente | Sentido de la conexion | Para que sirve |
|---|---|---|---|
| 8443/tcp | proxy (Docker, Nginx) | Navegador → contenedor | Proxy inverso HTTPS (certificado autofirmado); reparte a Element y a Synapse segun la ruta |
| 8008/tcp | synapse (Docker) | Element Desktop → contenedor | API cliente-servidor de Matrix, accesible tambien directo (sin HTTPS) para la app de escritorio |
| 5432/tcp | synapse-db (Docker, interno) | synapse → synapse-db | Conexion a PostgreSQL; no necesita exponerse al host |
| — | element (Docker, interno) | proxy → element | No publica ningun puerto al host; solo accesible a traves de proxy |
| 8448/tcp | No usado en esta guia | — | Puerto de federacion con otros homeservers; deliberadamente no expuesto |
13. Errores comunes y como resolverlos
| Problema | Causa probable | Solucion |
|---|---|---|
Synapse falla al arrancar con Database has incorrect collation... Should be 'C' |
El volumen de Postgres se creo sin POSTGRES_INITDB_ARGS, con el locale por defecto de la imagen |
Borrar el volumen (docker compose down -v) y volver a arrancar con el flag ya incluido en el docker-compose.yml (seccion 5) |
| Synapse no arranca, error de conexion a la base de datos | host en el bloque database: de homeserver.yaml apunta a localhost en vez de synapse-db, o las credenciales no coinciden con el .env |
Revisar el bloque database: caracter a caracter (seccion 6) |
El navegador o curl -k dan un error de TLS tipo alert internal error al conectar al proxy |
Fallo especifico de algunos entornos con el mecanismo de certificados de Caddy; no ocurre con Nginx | Esta guia ya usa Nginx con un certificado generado a mano precisamente para evitar este problema; si aparece igualmente, comprobar con openssl s_server si el propio sistema es capaz de completar un handshake TLS con esos mismos archivos, para descartar causas mas de base |
El contenedor proxy se reinicia en bucle con host not found in upstream "synapse" |
Nginx intento resolver el nombre del contenedor una sola vez al arrancar y no lo consiguio a tiempo | Usar resolver 127.0.0.11 junto con set $upstream_...; proxy_pass $upstream_...; en vez de un proxy_pass directo (seccion 7), para que la resolucion sea bajo demanda |
Tras cambiar el docker-compose.yml, sigue respondiendo el servicio antiguo en el mismo puerto |
Un contenedor de un servicio ya eliminado del compose sigue vivo (Compose no lo para si ya no aparece en el archivo) | docker ps para localizarlo, y docker stop <nombre> && docker rm <nombre> antes de volver a levantar el stack |
register_new_matrix_user falla o no encuentra el archivo de configuracion |
Ruta incorrecta en -c, o el contenedor synapse no esta corriendo todavia |
Confirmar con docker compose ps que synapse esta Up, y que la ruta es /data/homeserver.yaml (la ruta dentro del contenedor, no la del host) |
| Un usuario nuevo no puede registrarse el mismo desde Element | Comportamiento esperado: enable_registration esta en false a proposito |
Crear la cuenta con register_new_matrix_user (seccion 9) |
14. Notas de ampliacion
El montaje de esta guia cubre lo minimo necesario para tener un chat propio funcionando con cuentas locales, cifrado con HTTPS y acceso directo por IP. Para un entorno de uso continuado, conviene ampliar con:
- Importar el certificado en el almacen de confianza de cada equipo cliente (Windows, Mac, Linux), para que el aviso de "certificado no confiable" desaparezca de forma permanente en esos equipos, sin tener que aceptarlo cada vez.
- Dominio propio + Let's Encrypt en vez de certificado autofirmado, necesario si en algun momento se quiere activar la federacion con otros homeservers Matrix, que exige un certificado valido reconocido publicamente.
- Apps moviles (Element para Android e iOS), que pueden apuntar al mismo homeserver desde fuera de la red local una vez este detras de un dominio con HTTPS valido.
- Puentes (bridges) hacia otras plataformas como Slack, Discord o Telegram, para centralizar conversaciones de varias herramientas en el mismo cliente.
- Copias de seguridad del volumen de PostgreSQL y de la carpeta
synapse-data(incluye las claves de firma del servidor, necesarias para que el homeserver mantenga su identidad).