SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

Cómo crear un buzón de pruebas con Mailpit en un VPS

Recibe todos los correos de prueba en Mailpit, revísalos en la interfaz web y evita que tu aplicación de staging envíe mensajes a clientes reales.

Qué es una bandeja de entrada de correo desechable

Una bandeja de entrada de correo desechable es un servidor SMTP pequeño (simple mail transfer protocol) que acepta mensajes para cualquier dirección y no entrega ninguno. La aplicación de staging envía los mensajes a este servidor en lugar de enviarlos a un proveedor de correo real, y todos se detienen allí. Puede leer los mensajes recibidos en una interfaz web. Una lista de destinatarios incorrecta o una plantilla defectuosa no le cuesta nada, porque el correo nunca sale de la bandeja.

Esta guía crea una en un único VPS con Docker Compose. Mailpit actúa como receptor general. Su escucha SMTP está vinculada a una dirección a la que sólo puede acceder la aplicación, y su interfaz web queda detrás de nginx con seguridad de la capa de transporte (TLS) y una contraseña. Un límite de retención evita que el buzón llene el disco. Si Compose es nuevo para usted, los conceptos básicos de Compose para un VPS explican la estructura de archivos que presupone esta guía.

El resultado es una herramienta de pruebas, no un servidor de correo. No tiene cuentas, entrega de mensajes ni filtrado de spam. Los buzones reales para personas reales requieren un servidor de correo completo como Mailcow y constituyen un trabajo mucho mayor.

Mailpit frente a Inbucket frente a MailHog: qué sumidero ejecutar

Tres herramientas realizan esta tarea. Lo que las diferencia es su estado de mantenimiento, los puertos en los que escuchan y qué pueden hacer con un mensaje una vez que lo aceptan. Las versiones a continuación fueron verificadas en octubre de 2026.

MailHog (mailhog/mailhog) escucha en el puerto 1025 para SMTP y sirve su interfaz en el 8025. Todavía funciona. Su rama predeterminada no ha tenido commits desde agosto de 2022 y el rastreador contiene más de 200 incidencias abiertas, por lo que ejecutaría dependencias sin parches en su entorno de pruebas. No inicie nuevos proyectos con esta herramienta.

Inbucket (inbucket/inbucket) escucha en el puerto 2500 para SMTP, 9000 para la interfaz web y 1100 para POP3 (post office protocol version 3). La versión 3.2.0 se publicó el 10 de octubre de 2026. Almacena los mensajes como archivos en /storage y los depura automáticamente: la imagen establece INBUCKET_STORAGE_RETENTIONPERIOD=72h y INBUCKET_STORAGE_MAILBOXMSGCAP=300. Elíjalo cuando una prueba necesite recopilar correo con una biblioteca cliente POP3 en lugar de una llamada HTTP.

Mailpit (axllent/mailpit) utiliza los mismos puertos que MailHog, 1025 y 8025, por lo que reemplaza a MailHog sin modificar la configuración de la aplicación. La versión 1.31.6 se publicó el 15 de octubre de 2026; la línea 1.30 finalizó en la 1.30.7 en agosto. Incluye lo que esta guía necesita dentro del binario: un archivo de contraseñas para la interfaz web y la API (application programming interface), un límite de mensajes, un límite de antigüedad y un filtro de destinatarios. El resto de esta guía utiliza Mailpit.

Cómo funciona el catch-all y por qué no interviene DNS

La aplicación no busca aquí dónde entregar el mensaje. Se conecta a un host y un puerto, abre una conexión TCP y anuncia RCPT TO:<anyone@example.test>. Mailpit acepta ese destinatario tal como se indica, almacena el mensaje y no lo reenvía. El dominio nunca se resuelve, por lo que example.test funciona aunque .test sea un nombre reservado que no existe en el sistema de nombres de dominio (DNS).

Ese es todo el mecanismo y la razón por la que la bandeja de entrada es segura de forma predeterminada. No interviene ningún registro MX (Mail Exchanger), no se intenta ninguna entrega y ningún mensaje puede llegar a una persona real.

Dirija la aplicación de staging al sumidero

Establezca el host SMTP de la aplicación en mailpit cuando la aplicación se ejecute como contenedor en el mismo proyecto de Compose, o en 127.0.0.1 cuando se ejecute en el host. Establezca el puerto en 1025, desactive TLS y deje vacíos el nombre de usuario y la contraseña. Mailpit acepta correo anónimo.

Algunos frameworks no envían mensajes sin credenciales. MP_SMTP_AUTH_ACCEPT_ANY=1 hace que Mailpit acepte cualquier nombre de usuario y contraseña, y MP_SMTP_AUTH_ALLOW_INSECURE=1 permite los mecanismos PLAIN y LOGIN en una conexión sin cifrar. Estos dos ajustes son seguros aquí sólo porque el listener no es accesible desde Internet, algo que impone el despliegue siguiente.

Conviene establecer MP_SMTP_ALLOWED_RECIPIENTS desde el primer día. Este ajuste recibe una expresión regular y rechaza cualquier destinatario que no coincida con ella. Diríjalo a su dominio de pruebas. Así, si una base de datos de staging todavía contiene una dirección real de cliente, se producirá un error visible en el registro de la aplicación en lugar de un mensaje que llegue silenciosamente al sumidero.

El archivo Docker Compose

Cree primero el directorio y un archivo de contraseña para la interfaz web. htpasswd -B genera un hash bcrypt, y Mailpit lee tanto bcrypt como texto plano.

mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qa

Escriba compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.31
    container_name: mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
      - "127.0.0.1:1025:1025"
    volumes:
      - ./data:/data
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 2000
      MP_MAX_AGE: 14d
      MP_UI_AUTH_FILE: /data/ui-auth
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'

El signo de dólar doble no es un error. Compose interpreta un $ simple como el inicio de una variable a expandir, por lo que $$ es la forma de pasar un signo de dólar literal al contenedor. La expresión regular llega a Mailpit como @example\.test$.

Inícielo y compruebe el estado de salud:

docker compose up -d
docker compose ps

La columna STATUS debería mostrar Up ... (healthy). La imagen incluye su propio healthcheck que ejecuta /mailpit readyz cada 15 segundos, por lo que un contenedor que permanece en starting o cambia a unhealthy generalmente no está sirviendo en el puerto 8025 dentro del contenedor. La comprobación toma los ajustes de las variables de entorno MP_, pero no de los flags de línea de comandos; esta es una de las razones por las que esta guía configura todo bajo environment:. Si pasa las opciones como flags, la comprobación podría reportar unhealthy aunque Mailpit funcione normalmente. Lea docker compose logs mailpit antes de cambiar cualquier otra cosa.

Ambos puertos publicados incluyen una dirección, y esa dirección es el control de seguridad. Dentro del contenedor, Mailpit escucha en 0.0.0.0, lo cual es correcto porque el contenedor tiene su propio espacio de nombres de red. El lado izquierdo del mapeo determina quién accede desde el exterior. Escriba 8025:8025 y Docker vinculará todas las direcciones del host, incluida la pública.

Si su aplicación de staging es un servicio en este mismo archivo, elimine el mapeo 1025 por completo y apunte la aplicación al nombre de host mailpit en el puerto 1025. Los contenedores en una red compartida de Compose se comunican directamente entre sí, por lo que el puerto SMTP nunca llega a exponerse en el host. Cómo las redes de Compose resuelven nombres de servicio cubre esta resolución.

Enviar un mensaje y comprobar que se ha almacenado

python3 - <<'EOF'
import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
EOF

El script no muestra nada cuando la operación se completa correctamente. Confirme mediante la API que el mensaje se ha almacenado:

curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages

La respuesta devuelve un JSON con los mensajes almacenados. Si omite el indicador -u, la misma solicitud se rechaza porque MP_UI_AUTH_FILE protege la API y la interfaz web conjuntamente. Cualquier prueba que lea la bandeja de entrada también debe enviar esas credenciales.

Un ConnectionRefusedError del script de Python significa que no hay ningún proceso escuchando en 127.0.0.1:1025. Este es el resultado esperado si eliminó la asignación SMTP. En ese caso, la comprobación debe ejecutarse desde un contenedor conectado a la misma red de Compose.

Publicar la interfaz web a través de nginx con contraseña

La interfaz actualmente responde solo en la dirección de loopback. nginx termina TLS y solicita una contraseña antes de que cualquier petición llegue al servicio.

sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qa
server {
    listen 443 ssl;
    server_name mail-test.example.com;

    ssl_certificate     /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;

    auth_basic           "mailpit";
    auth_basic_user_file /etc/nginx/mailpit.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:8025;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Recargue la configuración tras verificar la sintaxis con sudo nginx -t && sudo systemctl reload nginx. Qué hace cada directiva en un bloque de reverse proxy merece una lectura si es su primer proxy.

Utilice el mismo nombre de usuario y contraseña en el archivo de nginx y en data/ui-auth. nginx reenvía la cabecera Authorization del navegador al backend, por lo que unas credenciales coincidentes satisfacen ambas comprobaciones con un solo aviso. Si las credenciales son distintas, el navegador conservará un conjunto que la segunda comprobación rechazará.

Las cabeceras Upgrade y Connection no son decorativas. Mailpit envía correos nuevos a una página abierta mediante un WebSocket, y un proxy que ejecute HTTP/1.1 sin esas cabeceras no puede actualizar la conexión. La página carga correctamente pero nunca se actualiza: el correo llega, la API lo muestra, pero la lista permanece estática hasta que recarga.

Mantenga ambos bloqueos. La contraseña de nginx protege la dirección pública y MP_UI_AUTH_FILE protege el puerto 8025 en sí mismo; esto es importante porque cada enlace de restablecimiento de contraseña que su aplicación de staging haya generado, mientras la retención lo mantenga, es legible en esa interfaz.

Nunca permita que el sink se convierta en un open relay

Un open relay es un servidor SMTP que acepta un mensaje de cualquier remitente y lo reenvía a cualquier destino. Los spammers los buscan constantemente; encontrar uno en su dirección resulta en reportes de abuso y la suspensión de la cuenta.

Mailpit no es un open relay por defecto, ya que no reenvía nada automáticamente. Tiene dos funciones de salida y ambas permanecen desactivadas hasta que usted las configura. MP_SMTP_RELAY_CONFIG apunta a un archivo de configuración de relay y habilita la acción de liberación en la interfaz, la cual envía el mensaje seleccionado a través de ese servidor; el reenvío automático requiere un parámetro adicional, MP_SMTP_RELAY_ALL (falso por defecto) o MP_SMTP_RELAY_MATCHING. El reenvío, configurado con MP_SMTP_FORWARD_CONFIG o las variables MP_SMTP_FORWARD_*, copia cada mensaje entrante a direcciones fijas que usted elija. Dejar todas estas opciones sin configurar es una decisión deliberada.

Existen dos formas de perder esta propiedad. Active MP_SMTP_RELAY_ALL, que envía cada mensaje entrante a sus destinatarios originales e ignora MP_SMTP_ALLOWED_RECIPIENTS, y luego exponga el puerto SMTP a internet; habrá creado un open relay funcional. MP_SMTP_RELAY_MATCHING hace lo mismo para cualquier destinatario que coincida con su patrón. Si expone el puerto sin relay, los desconocidos no podrán enviar correos a través de usted, pero podrán llenar su almacenamiento e introducir contenido en la interfaz en la que confía su equipo. Con el reenvío activado, sus mensajes también llegarán a cualquier bandeja de entrada que reciba las copias.

La trampa en un host Docker es el firewall. Publicar un puerto hace que Docker escriba sus propias reglas en la tabla nat, y el tráfico destinado al contenedor se procesa allí antes de que las reglas de ufw (uncomplicated firewall) tengan efecto. sudo ufw deny 1025/tcp reporta éxito y no cambia nada. Por qué Docker publica puertos saltándose ufw explica el orden de la cadena.

La solución es la dirección en el mapeo, no una regla de firewall. Compruebe qué está vinculado realmente:

sudo ss -ltnp | grep -E ':(1025|8025)'

Una salida saludable muestra 127.0.0.1:1025 y 127.0.0.1:8025. Una línea que indique 0.0.0.0:1025 significa que el mapeo perdió su dirección y el sink está escuchando en internet. Desde otra máquina, nc -vz mail-test.example.com 1025 debería agotar el tiempo de espera o ser rechazado.

Cuando la aplicación reside en un servidor diferente, no abra el puerto 1025 para conectar ambos. Coloque ambas máquinas en una red privada o en un túnel VPN, y vincule el mapeo a la dirección de esa interfaz.

Publique registros MX solo si desea recibir correo real

Un registro MX (mail exchanger) indica a otros servidores de correo qué host acepta el correo para un dominio. Omitirlo no cierra la puerta por sí solo: cuando un dominio no tiene registro MX, los servidores emisores recurren a su registro de dirección (A o AAAA), tal como especifica la sección 5.1 del RFC 5321, y el nombre de host que apunta a este equipo para la interfaz web tiene uno. Lo que impide la entrada de correo de internet es que no hay nada en el equipo escuchando en el puerto 25. El buzón solo contiene lo que sus propias aplicaciones han enviado, que es para lo que sirve un buzón de pruebas.

Recibir correo real implica tener un registro DNS que dirija a los servidores emisores hacia el equipo (un registro MX o el registro de dirección al que recurren), Mailpit escuchando en el puerto 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) y ese puerto abierto. En ese momento, usted está ejecutando un sistema de recepción general (catch-all) público para cada dirección del dominio. Tenga claras las consecuencias:

  • El spam comienza a los pocos días de aparecer el registro, porque los recolectores leen el DNS. Los ataques de diccionario prueban nombres comunes y almacenan un mensaje por cada intento.
  • Los archivos adjuntos de desconocidos aterrizan en su disco y permanecen allí. Nada los filtra, por lo que un archivo de un remitente desconocido se almacena junto a su propio correo de prueba.
  • Cualquiera que conozca el dominio puede registrarse en servicios de terceros con una dirección del mismo, y el correo de confirmación se entregará en su servidor. Si la protección por contraseña falla, esas cuentas pertenecerán a quien esté leyendo el buzón.
  • Los límites de retención dejan de ser una tarea de mantenimiento y se vuelven críticos, porque el volumen ya no está bajo su control.

Si necesita recibir correo real para una comprobación de entregabilidad, utilice un subdominio dedicado, mantenga MP_MAX_AGE breve y trate todo su contenido como público. Si necesita buzones de correo en los que los usuarios confíen, ejecute un servidor de correo real con filtrado y copias de seguridad.

Retención: cómo un catch-all sin límites llena el disco

Mailpit mantiene 500 mensajes de forma predeterminada y elimina periódicamente los más antiguos que excedan esa cifra. MP_MAX_MESSAGES: 0 desactiva ese límite y, sin un MP_MAX_AGE configurado, no se elimina nada automáticamente, que es como un catch-all llena un disco sin que nadie lo note. MP_MAX_AGE añade un límite de tiempo, funciona junto con o en lugar del límite de mensajes, y acepta horas o días, escritos como 36h o 14d.

MP_DATABASE decide si algo de esto sobrevive. Sin él, Mailpit escribe en un archivo temporal que se elimina cuando el proceso termina, por lo que cada reinicio vacía la bandeja de entrada. Con él, el correo sobrevive a los reinicios y el archivo crece.

Los archivos adjuntos son los que consumen el espacio. Un trabajo nocturno que envía un informe PDF de 2 MB a 300 direcciones de prueba supone 600 MB por noche, y un límite de cantidad de mensajes por sí solo no reaccionará a tiempo. Calcule ese crecimiento frente a cualquier otro elemento que comparta el volumen, ya que un vecino con gran carga multimedia como PhotoPrism o Immich ya habrá reclamado la mayor parte del disco de un VPS pequeño.

du -h ~/mailpit/data/mailpit.db
df -h /

Vacíe el almacén entre ejecuciones de CI en lugar de esperar a que se active un límite:

curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages

Inbucket gestiona el mismo problema con INBUCKET_STORAGE_RETENTIONPERIOD (72h en la imagen) y INBUCKET_STORAGE_MAILBOXMSGCAP (300). Independientemente de cuál ejecute, elija el límite antes de que la primera suite de pruebas apunte a él.

Lectura de la bandeja de entrada desde la suite de pruebas

GET /api/v1/messages muestra lo almacenado, GET /api/v1/message/{ID} devuelve un mensaje con sus partes y cabeceras, GET /api/v1/search aplica filtros y DELETE /api/v1/messages vacía el almacén. La documentación interactiva de la versión en ejecución está disponible en http://127.0.0.1:8025/api/v1/.

Una prueba útil envía un mensaje, consulta periódicamente hasta que aparece, comprueba el asunto y el enlace que contiene, y después elimina todo. Use un bucle corto de reintentos para las consultas en lugar de una única solicitud, porque una aplicación que pone el correo en cola mediante un worker en segundo plano devuelve el control de la llamada de envío antes de que Mailpit reciba el mensaje. El mismo patrón aparece en herramientas autoalojadas para probar y simular API, que suelen ser la otra parte de un entorno de staging que nunca toca producción.

FAQ

¿Es un buzón de correo temporal autohospedado un open relay?

No, mientras el reenvío permanezca desactivado. Por defecto, Mailpit no envía nada hacia afuera: MP_SMTP_RELAY_CONFIG solo permite la liberación manual de los mensajes que usted elija, y el reenvío automático requiere MP_SMTP_RELAY_ALL o MP_SMTP_RELAY_MATCHING adicionales. Si deja estos parámetros sin configurar, un extraño que acceda al puerto 1025 no podrá enviar correo a través de su servidor. Aun así, pueden llenar su almacenamiento, por lo que debe enlazar el puerto SMTP a una dirección a la que solo su aplicación pueda acceder. Publicarlo como 1025:1025 en Compose enlaza todas las direcciones del host, y sudo ufw deny 1025/tcp no lo cerrará, ya que las reglas nat de Docker tienen prioridad.

¿Necesito un registro MX para mi dominio de pruebas?

Solo si desea que llegue correo desde Internet; omitirlo no es una medida de seguridad por sí misma. Sin un registro MX, los servidores emisores recurren al registro A o AAAA del dominio, por lo que lo que realmente impide la entrada de correo de Internet es que no haya nada escuchando en el puerto 25. El buzón solo contendrá lo que sus propias aplicaciones envíen a través de SMTP. Si apunta el DNS a la caja y abre el puerto 25, estará ejecutando un catch-all público: recibirá spam en cuestión de días, ataques de diccionario que almacenan un mensaje por intento y archivos adjuntos de desconocidos en su disco sin ningún filtrado.

¿Por qué la lista de mensajes solo se actualiza al recargar la página?

Mailpit envía el correo nuevo a una página abierta mediante un WebSocket. Un bloque location de nginx al que le falten proxy_http_version 1.1 y las cabeceras Upgrade y Connection no puede actualizar esa conexión, por lo que la página carga normalmente y luego se congela. El correo sigue llegando y la API sigue devolviéndolo, razón por la cual el buzón parece desactualizado en lugar de roto. Añada esas líneas, recargue nginx y luego recargue la página.

¿Cómo evito que el buzón llene el disco?

Mantenga MP_MAX_MESSAGES en un número real y añada MP_MAX_AGE. El límite por defecto es de 500 mensajes, y establecerlo en 0 deshabilita la eliminación basada en conteo, por lo que, a menos que MP_MAX_AGE esté configurado, no se purgará nada; así es como un catch-all con archivos adjuntos crece silenciosamente. MP_MAX_AGE acepta horas o días, como 36h o 14d. Limpie el almacenamiento en el desmantelamiento de CI con curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages, ya que el endpoint de eliminación utiliza las mismas credenciales que la interfaz web. Solo la API de envío (POST /api/v1/send) puede tener sus propias credenciales, y esto es opcional. Inbucket realiza la misma tarea con INBUCKET_STORAGE_RETENTIONPERIOD (72h) y INBUCKET_STORAGE_MAILBOXMSGCAP (300).

¿Debería ejecutar Mailpit, Inbucket o MailHog?

Mailpit para trabajos nuevos, a fecha de octubre de 2026. MailHog sigue funcionando, pero su rama por defecto no ha tenido commits desde agosto de 2022, por lo que distribuye dependencias sin parches. Inbucket se mantiene activamente (3.2.0, octubre de 2026) y es la mejor opción cuando una prueba requiere POP3, ya que el servidor POP3 de Mailpit solo se inicia una vez que se le proporcionan credenciales, ya sea en un archivo de contraseñas o en la variable MP_POP3_AUTH. Mailpit utiliza los mismos puertos que MailHog, 1025 y 8025, por lo que reemplazar MailHog solo requiere cambiar un nombre de imagen en su archivo Compose.