¡Muy buenas!
Hoy vamos a montar un servidor de correo completo para vbits.me, desde un VPS recién preparado hasta el momento en que un mensaje sale por SMTP directo y llega a Proton Mail.
La idea es que esta guía sea una receta reproducible. Si utilizas otro dominio, cambia vbits.me, los hostnames, la IP pública y las contraseñas; el resto de la estructura es la misma.
Al final tendremos Stalwart para el correo, SnappyMail como webmail, Nginx Proxy Manager para HTTPS y SPF, DKIM, DMARC y TLS correctamente configurados.
Y una petición amistosa: úsalo para tu correo y no para hacer spam, que luego la reputación la pagamos todos. 😉
1. Valores de esta instalación
Dominio: vbits.me
Correo: mail.vbits.me
Webmail: webmail.vbits.me
Panel privado: mailadmin.vbits.me
IP pública: 94.143.142.166
DNS autoritativo: IONOS
Proveedor VPS: Piensa Solutions
Stalwart: v0.16 Community
El panel administrativo se publica solo por la VPN. No expongas la administración de Stalwart directamente a Internet.
2. Preparar el VPS
Esta guía usa Debian/Ubuntu y Docker. Si Docker ya está instalado, puedes saltar al apartado de puertos.
sudo apt update
sudo apt install -y ca-certificates curl gnupg nftables openssl dnsutils
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
docker version
docker compose version
Comprobamos que no haya otro MTA ocupando los puertos:
sudo ss -ltnp | grep -E ':(25|465|587|993|4190)\b' || true
sudo systemctl disable --now postfix # solo si no se va a conservar Postfix
3. Firewall y activación del puerto 25
Antes de tocar nftables guarda una copia:
sudo cp -a /etc/nftables.conf \
/etc/nftables.conf.before-mail-$(date +%Y%m%d)
Con una política drop, permite TCP 25, 465, 587, 993, 4190, además de SSH, WireGuard y HTTP/HTTPS según tu arquitectura. Si Docker publica los puertos, permite también el tráfico correspondiente en forward.
Valida la configuración antes de cargarla:
sudo nft -c -f /etc/nftables.conf
El firewall del proveedor es independiente. Hay que solicitar a Piensa la salida SMTP por TCP/25:
Necesito activar el envío SMTP saliente por TCP/25 para mi VPS. Se utilizará para un dominio propio con SPF, DKIM y DMARC. También necesito configurar el PTR de la IP para que apunte a
mail.vbits.me.
Comprueba la salida desde el VPS, no solo la entrada:
nc -vz -w 10 gmail-smtp-in.l.google.com 25
nc -vz -w 10 mail.protonmail.ch 25
4. DNS en IONOS
La zona de vbits.me se gestiona directamente en IONOS. Crea estos registros:
A mail 94.143.142.166
A webmail 94.143.142.166
MX @ 5 mail.vbits.me.
TXT @ v=spf1 ip4:94.143.142.166 include:_spf-eu.ionos.com ~all
TXT _dmarc v=DMARC1; p=none; rua=mailto:admin@vbits.me; adkim=s; aspf=s; pct=100
Debe existir un único SPF. El include conserva la autorización de IONOS mientras comprobamos si todavía hay otros servicios que envían mensajes.
Stalwart genera DKIM. Publica exactamente los TXT que muestre el panel. En esta instalación los selectores son:
dig +short TXT v1-ed25519-20261023._domainkey.vbits.me
dig +short TXT v1-rsa-20261023._domainkey.vbits.me
No reutilices las claves DKIM de otra instalación: cada despliegue puede generar selectores diferentes.
5. Crear las redes Docker
docker network create mail 2>/dev/null || true
docker network inspect npm_frontend >/dev/null 2>&1 || \
echo 'La red npm_frontend debe existir antes de continuar'
mail será la red interna de correo. npm_frontend permite que Nginx Proxy Manager llegue al webmail.
6. Desplegar Stalwart
sudo mkdir -p /opt/stalwart/{config,data}
cd /opt/stalwart
Guarda este docker-compose.yml:
services:
stalwart:
image: stalwartlabs/stalwart:v0.16
container_name: stalwart
restart: unless-stopped
environment:
STALWART_PUBLIC_URL: https://mail.vbits.me
ports:
- "25:25"
- "465:465"
- "587:587"
- "993:993"
- "4190:4190"
- "<IP_PRIVADA_VPN>:8080:8080"
- "<IP_PRIVADA_VPN>:8443:443"
volumes:
- ./config:/etc/stalwart
- ./data:/var/lib/stalwart
networks:
- mail
- npm_frontend
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
networks:
mail:
external: true
npm_frontend:
external: true
La IP <IP_PRIVADA_VPN> es la interfaz VPN usada en este VPS. Sustitúyela por tu interfaz de administración; no publiques 8080 ni 8443 en Internet.
docker compose config --quiet
docker compose up -d
docker ps --filter name=stalwart
docker logs --since 5m stalwart
En el panel privado crea la cuenta administrativa, el dominio vbits.me, las cuentas de correo y las claves DKIM:
https://mailadmin.vbits.me/admin
7. Configurar los listeners
En Settings → Network → Listeners comprobamos estos listeners:
| Nombre | Puerto | Protocolo | TLS |
|---|---|---|---|
| smtp | 25 | SMTP | STARTTLS opcional |
| submissions | 465 | SMTP | TLS implícito |
| submission | 587 | SMTP | STARTTLS |
| imaps | 993 | IMAP | TLS implícito |
| sieve | 4190 | ManageSieve | STARTTLS |
ss -ltnp | grep -E ':(25|465|587|993|4190)\b'
En Stalwart v0.16 algunos listeners pueden estar guardados pero no aplicados al proceso hasta recargar o recrear el contenedor.
8. Desplegar SnappyMail
sudo mkdir -p /opt/snappymail/data
cd /opt/snappymail
docker-compose.yml:
services:
snappymail:
image: djmaze/snappymail@sha256:cc053169c76e27d54c3533b002ec9987786ca50bf9265ae318b44d8699f60ec0
container_name: snappymail
restart: unless-stopped
expose:
- "8888"
volumes:
- ./data:/var/lib/snappymail
networks:
- mail
- npm_frontend
networks:
mail:
external: true
npm_frontend:
external: true
docker compose config --quiet
docker compose up -d
docker ps --filter name=snappymail
En Nginx Proxy Manager crea un Proxy Host:
Domain Names: webmail.vbits.me
Forward Hostname: snappymail
Forward Port: 8888
Websockets Support: activado
Solicita un certificado para webmail.vbits.me, activa Force SSL y prueba:
curl -I https://webmail.vbits.me
En SnappyMail configura el dominio así:
IMAP: stalwart:993 IMAPS
SMTP: stalwart:465 SMTPS y autenticación
Sieve: stalwart:4190 STARTTLS
Prueba el inicio de sesión y un envío local antes de probar la entrega externa.
9. Certificado TLS para el correo
El certificado de webmail.vbits.me no sirve automáticamente para SMTP. Emitimos otro para mail.vbits.me usando el webroot HTTP de Nginx Proxy Manager:
sudo apt install -y certbot
sudo certbot certonly \
--webroot \
-w /opt/nginx-proxy-manager/data/letsencrypt-acme-challenge \
-d mail.vbits.me \
--email admin@vbits.me \
--agree-tos \
--no-eff-email \
--non-interactive \
--keep-until-expiring
Comprobamos el certificado sin mostrar nunca la clave privada:
sudo certbot certificates
sudo openssl x509 \
-in /etc/letsencrypt/live/mail.vbits.me/fullchain.pem \
-noout -subject -issuer -dates -ext subjectAltName
Añade al Compose de Stalwart los montajes de solo lectura:
- /etc/letsencrypt/live/mail.vbits.me:/etc/letsencrypt/live/mail.vbits.me:ro
- /etc/letsencrypt/archive/mail.vbits.me:/etc/letsencrypt/archive/mail.vbits.me:ro
Conserva una copia, valida y recrea el servicio:
cp -a docker-compose.yml docker-compose.yml.before-tls
docker compose config --quiet
docker compose up -d --force-recreate stalwart
Si el contenedor usa UID 2000, concede solo lectura a la clave del certificado:
sudo setfacl -m u:2000:r /etc/letsencrypt/archive/mail.vbits.me/privkey*.pem
sudo setfacl -m d:u:2000:r,d:m:r /etc/letsencrypt/archive/mail.vbits.me
En Settings → TLS → Certificates crea un certificado con:
Certificate: /etc/letsencrypt/live/mail.vbits.me/fullchain.pem
Private key: /etc/letsencrypt/live/mail.vbits.me/privkey.pem
Solicita la recarga TLS desde la administración y verifica:
openssl s_client -starttls smtp -connect mail.vbits.me:25 \
-servername mail.vbits.me </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
openssl s_client -connect mail.vbits.me:465 \
-servername mail.vbits.me </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
openssl s_client -connect mail.vbits.me:993 \
-servername mail.vbits.me </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
La renovación de Certbot debe incluir una recarga de certificados de Stalwart mediante un deploy hook. No copies claves privadas al repositorio ni al artículo.
10. PTR y reputación
El PTR lo configura Piensa, no IONOS. En esta instalación ya está asignado por el proveedor:
94.143.142.166 → mail.vbits.me
La resolución inversa se verifica desde fuera del VPS:
dig +short -x 94.143.142.166
Debe devolver mail.vbits.me.. El banner SMTP y el PTR ya coinciden.
El PTR no cambia webs ni enrutamiento; solo establece el nombre inverso de la IP. Una IP solo puede tener un PTR.
Una entrada UCEPROTECT de nivel 3 suele referirse al rango del proveedor. No conviene pagar una eliminación automática sin pedir primero una revisión a Piensa.
11. Verificación de entrega
dig +short MX vbits.me
dig +short A mail.vbits.me
dig +short TXT vbits.me
dig +short TXT _dmarc.vbits.me
dig +short TXT v1-ed25519-20261023._domainkey.vbits.me
dig +short -x 94.143.142.166
nc -vz -w 10 mail.vbits.me 25
nc -vz -w 10 mail.vbits.me 465
nc -vz -w 10 mail.vbits.me 587
nc -vz -w 10 mail.vbits.me 993
nc -vz -w 10 mail.vbits.me 4190
Desde SnappyMail envía a Gmail, Outlook y Proton Mail. En los encabezados del mensaje recibido comprueba:
spf=pass
dkim=pass
dmarc=pass
La prueba final es que el MX remoto responda 250 Ok y que la cola remota de Stalwart quede vacía.
12. Reintentos y mensajes pendientes
Un timeout no elimina un mensaje. Stalwart lo conserva y aplica reintentos progresivos hasta entregarlo, generar un rebote o alcanzar la caducidad.
La programación de reintentos y la expiración deben revisarse juntas. En este montaje, después de varios fallos por el puerto 25 bloqueado, el siguiente reintento quedó programado después de la caducidad.
Cuando se corrige la causa, se puede adelantar el siguiente reintento desde Management → Emails → Queued. Esto no borra el mensaje.
13. DMARC: observar y endurecer
Durante los primeros días mantenemos p=none y revisamos los informes rua. Si solo aparecen emisores esperados y SPF/DKIM están alineados, pasamos a:
v=DMARC1; p=quarantine; rua=mailto:admin@vbits.me; adkim=s; aspf=s; pct=100
Después, si todo continúa correcto:
v=DMARC1; p=reject; rua=mailto:admin@vbits.me; adkim=s; aspf=s; pct=100
BIMI es opcional. Requiere quarantine o reject al 100 %, además del registro BIMI y, para una aceptación amplia, la certificación del logo.
14. Problemas encontrados
- Piensa bloqueaba inicialmente la salida por el puerto 25.
- Dos mensajes quedaron en cola mientras el bloqueo estaba activo.
- El siguiente reintento quedaba después de la caducidad y se adelantó manualmente.
- El PTR genérico del proveedor no coincidía inicialmente con el banner SMTP; Piensa lo actualizó a
mail.vbits.me. - El primer certificado TLS de Stalwart era autofirmado.
- SPF y DMARC no existían al principio y se añadieron desde IONOS.
15. Checklist final
- [x] MX apuntando a
mail.vbits.me. - [x] A de
mail.vbits.mecorrecto. - [x] PTR coincidente con
mail.vbits.me. - [x] SPF publicado.
- [x] DKIM publicado.
- [x] DMARC en modo
none. - [x] Certificado TLS válido.
- [x] SMTP, SMTPS, submission, IMAPS y ManageSieve activos.
- [x] Salida directa por el puerto 25.
- [x] Entrega real probada.
- [x] Cola vacía después de entregar.
- [x] Renovación TLS automatizada.
16. Proteger la administración de WordPress
El sitio público puede seguir siendo accesible desde Internet sin dejar expuesto el panel de WordPress. En Nginx Proxy Manager restringimos estas rutas del proxy de vbits.me a la red WireGuard 10.8.0.0/24:
/wp-login.php
/wp-admin/
Desde Internet ambas rutas devuelven 403. Al conectarse a WireGuard, el formulario de login vuelve a estar disponible y /wp-admin/ puede redirigir al login si todavía no hay una sesión autenticada.
La excepción /wp-admin/admin-ajax.php permanece pública porque el tema utiliza esa ruta para algunas funciones del frontend. No concede acceso al panel y una petición sin acción devuelve 400.
Después de cambiar la configuración hay que validar Nginx y probar los dos caminos:
docker exec nginx-proxy-manager nginx -t
curl -I https://vbits.me/wp-login.php
curl -I --resolve vbits.me:443:10.8.0.1 https://vbits.me/wp-login.php
Despedida
Y con esto ya tenemos un servidor de correo propio, con webmail, cifrado, autenticación DNS y entrega directa. La parte divertida es verlo funcionar; la parte responsable es vigilar los informes, mantener todo actualizado y no convertirlo en una máquina de spam.
¡Que lo disfrutes y que tus correos lleguen a la bandeja de entrada! 😉