SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-21

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 y MailHog: qué sink ejecutar

Tres herramientas realizan esta función. Se diferencian por su estado de mantenimiento, los puertos en los que escuchan y las operaciones que pueden realizar con un mensaje después de aceptarlo. Las versiones siguientes se comprobaron en agosto de 2026.

MailHog (mailhog/mailhog) escucha en 1025 para SMTP y sirve su interfaz en 8025. Todavía funciona. Su rama predeterminada no recibe ningún commit desde agosto de 2022 y el sistema de seguimiento contiene más de 250 incidencias abiertas, por lo que ejecutaría dependencias sin parches en la ruta de pruebas. No inicie proyectos nuevos con él.

Inbucket (inbucket/inbucket) escucha en 2500 para SMTP, en 9000 para la interfaz web y en 1100 para POP3 (protocolo de oficina de correos versión 3). La versión 3.1.1 se publicó en diciembre de 2025. Almacena los mensajes como archivos en /storage y los elimina automáticamente según su antigüedad: 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 sustituye a MailHog sin modificar la configuración de la aplicación. La versión 1.30.7 se publicó el 8 de agosto de 2026. Incluye en el binario todo lo que necesita esta guía: un archivo de contraseñas para la interfaz web y la API (interfaz de programación de aplicaciones), un límite de mensajes, un límite de antigüedad y un filtro de destinatarios. El resto de esta guía ejecuta 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 de Docker Compose

Cree primero el directorio y un archivo de contraseñas para la interfaz web. htpasswd -B escribe un hash bcrypt, y Mailpit también lee bcrypt y texto sin formato.

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.30
    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 duplicado no es un error tipográfico. Compose interpreta un solo $ como el inicio de una variable que debe expandir, por lo que $$ permite 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 debe mostrar Up ... (healthy). La imagen incluye su propio healthcheck, que ejecuta /mailpit readyz cada 15 segundos. Por tanto, un contenedor que permanece en starting o cambia a unhealthy no está ofreciendo el servicio en 8025 dentro del contenedor. 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 de la asignación determina quién puede acceder desde fuera. Escriba 8025:8025 y Docker enlazará todas las direcciones del host, incluida la dirección pública.

Si su aplicación de staging es un servicio de este mismo archivo, elimine por completo la asignación 1025 y apunte la aplicación al nombre de host mailpit en el puerto 1025. Los contenedores de una red compartida de Compose se conectan directamente entre sí, por lo que el puerto SMTP no llega al host. Cómo resuelven los nombres de servicio las redes de Compose explica esa 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 una contraseña

La interfaz actualmente sólo responde en la dirección de loopback. nginx termina TLS y solicita una contraseña antes de que ninguna petición llegue a ella.

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;
    }
}

Vuelva a cargar la configuración después de comprobar la sintaxis con sudo nginx -t && sudo systemctl reload nginx. Qué hace cada directiva en un bloque de proxy inverso merece una lectura si es la primera vez que configura un proxy.

Use el mismo nombre de usuario y la misma contraseña en el archivo de nginx y en data/ui-auth. nginx reenvía al backend la cabecera Authorization del navegador, por lo que unas credenciales coincidentes satisfacen ambas comprobaciones con una sola solicitud. Si usa credenciales diferentes, el navegador conserva un conjunto que la segunda comprobación rechaza.

Las cabeceras Upgrade y Connection no son decorativas. Mailpit envía los mensajes nuevos a una página abierta mediante un WebSocket, y un proxy que ejecuta HTTP/1.1 sin esas cabeceras no puede actualizar la conexión. La página se carga correctamente, pero después no cambia: llegan mensajes, la API los muestra y la lista permanece igual hasta que vuelve a cargarla.

Mantenga ambos controles. La contraseña de nginx protege la dirección pública y MP_UI_AUTH_FILE protege directamente el puerto 8025. Esto es importante porque todos los enlaces de restablecimiento de contraseña que su aplicación de staging haya generado alguna vez se pueden leer en esa interfaz.

Nunca permita que el sink se convierta en un relay abierto

Un relay abierto es un servidor SMTP que acepta mensajes de cualquier persona y los reenvía a cualquier destino. Los spammers los buscan constantemente. Encontrar uno en su dirección termina en informes de abuso y en la suspensión de la cuenta.

Mailpit no es un relay abierto de forma predeterminada porque nunca reenvía mensajes. El relay permanece desactivado hasta que se indica a MP_SMTP_RELAY_CONFIG un archivo de configuración del relay. La acción de release de la interfaz tampoco hace nada hasta entonces. Dejar este valor sin configurar es una decisión deliberada.

Hay dos formas de perder esta propiedad. Configure un relay para que funcione el botón de release y después exponga el puerto SMTP a Internet. Así habrá creado un relay abierto operativo. Si expone el puerto sin configurar un relay, los extraños no podrán enviar correo a través de su servidor, pero sí llenar el almacenamiento e introducir contenido en la interfaz en la que confía su equipo.

En un host Docker, el problema suele ser el firewall. Al publicar un puerto, Docker escribe sus propias reglas en la tabla nat. El tráfico destinado al contenedor se procesa allí antes de que las reglas de ufw (uncomplicated firewall) puedan aplicarse. sudo ufw deny 1025/tcp informa de que la operación tuvo éxito, pero no cambia nada. Por qué Docker publica puertos directamente sin pasar por ufw explica el orden de la cadena.

La solución está en la dirección del mapeo, no en una regla del firewall. Compruebe qué está enlazado realmente:

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

Una salida correcta muestra 127.0.0.1:1025 y 127.0.0.1:8025. Una línea con 0.0.0.0:1025 indica que el mapeo perdió su dirección y que 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 está en otro servidor, no abra 1025 para conectar ambos. Coloque las dos máquinas en una red privada o en un túnel VPN, y enlace el mapeo a la dirección de esa interfaz.

Publica registros MX únicamente si necesitas recibir correo real

Un registro MX (mail exchanger) indica a otros servidores de correo qué host acepta el correo de un dominio. Si tu dominio desechable no tiene un registro MX, no puede llegar ningún mensaje desde Internet, porque los servidores emisores no tienen dónde entregarlo. La bandeja de entrada sólo contiene lo que enviaron tus propias aplicaciones. Ese es el objetivo de un buzón de pruebas.

Recibir correo real requiere un registro MX que apunte al servidor, que Mailpit escuche en el puerto 25 (MP_SMTP_BIND_ADDR=0.0.0.0:25) y que ese puerto esté abierto. A partir de ese momento, ejecutas un receptor público para cualquier dirección del dominio. Debes tener claras las consecuencias.

  • El spam empieza en pocos días desde la publicación del registro, porque los recolectores consultan DNS. Después llegan ataques de diccionario que prueban nombres habituales y almacenan un mensaje por cada intento.
  • Los archivos adjuntos de remitentes desconocidos llegan al disco y permanecen allí. No hay ningún filtro que los bloquee, por lo que un archivo comprimido de un remitente desconocido queda junto a tus propios mensajes de prueba.
  • Cualquiera que conozca el dominio puede registrarse en servicios de terceros con una dirección de ese dominio, y el mensaje de confirmación se entrega en tu servidor. Si alguna vez se elude la protección mediante contraseña, esas cuentas pertenecen a quien lea la bandeja de entrada.
  • Los límites de retención dejan de ser una tarea de mantenimiento y pasan a ser un componente crítico, porque ya no puedes controlar el volumen.

Si necesitas recibir correo real para comprobar la entregabilidad, asígnale un subdominio dedicado, mantén MP_MAX_AGE corto y trata todo su contenido como público. Si necesitas buzones de los que dependan otras personas, ejecuta 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 conserva 500 mensajes de forma predeterminada y elimina periódicamente los más antiguos que superan ese límite. MP_MAX_MESSAGES: 0 desactiva por completo la eliminación automática, y ese único cambio hace que un catch-all llene el disco sin que nadie lo detecte. MP_MAX_AGE añade un límite de tiempo, con una duración de horas o días, escrita como 36h o 14d.

MP_DATABASE determina si estos datos sobreviven. Sin esta opción, Mailpit escribe en un archivo temporal que se elimina cuando termina el proceso, por lo que cada reinicio vacía la bandeja de entrada. Con ella, los mensajes sobreviven a los reinicios y el archivo crece.

Los archivos adjuntos son los que consumen espacio. Un trabajo nocturno que envía un informe PDF de 2 MB a 300 direcciones de prueba consume 600 MB por noche, y un límite basado sólo en el número de mensajes no reaccionará a tiempo. Calcule este crecimiento junto con el consumo del resto de servicios que compartan el volumen, porque un vecino que use muchos archivos multimedia, como PhotoPrism o Immich, ya habrá ocupado 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 resuelve el mismo problema con INBUCKET_STORAGE_RETENTIONPERIOD (72h en la imagen) y INBUCKET_STORAGE_MAILBOXMSGCAP (300). Con independencia de cuál use, establezca el límite antes de que la primera suite de pruebas lo utilice.

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 desechable autohospedado un relay abierto?

No mientras el relay permanezca desactivado. Mailpit almacena los mensajes y nunca los reenvía hasta que se configura MP_SMTP_RELAY_CONFIG con un relay, por lo que un tercero que llegue al puerto 1025 no puede enviar correo a través del servidor. Aun así, puede llenar el almacenamiento. Por eso, vincule el puerto SMTP a una dirección a la que sólo pueda acceder la aplicación. Publicarlo como 1025:1025 en Compose lo vincula a todas las direcciones del host, y sudo ufw deny 1025/tcp no lo cerrará porque las reglas nat propias de Docker se procesan primero.

¿Necesito un registro MX para mi dominio de prueba?

Sólo si quiere recibir correo desde Internet. Sin un registro MX, los servidores emisores no tienen dónde entregar los mensajes. Por tanto, el buzón sólo contiene los mensajes que sus propias aplicaciones envían mediante SMTP. Publique el registro y abra el puerto 25, y estará ejecutando un catch-all público: recibirá spam en pocos días, ataques de diccionario que almacenarán un mensaje por intento y archivos adjuntos de desconocidos en el disco, sin ningún filtrado.

¿Por qué la lista de mensajes sólo se actualiza cuando recargo la página?

Mailpit envía los mensajes nuevos a una página abierta mediante un WebSocket. Un bloque location de nginx que no incluya proxy_http_version 1.1 ni las cabeceras Upgrade y Connection no puede actualizar esa conexión. La página se carga con normalidad y después deja de actualizarse. El correo sigue llegando y la API sigue devolviéndolo. Por eso el buzón parece desactualizado y no averiado. Añada esas líneas, recargue nginx y vuelva a cargar la página.

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

Mantenga MP_MAX_MESSAGES con un valor real y añada MP_MAX_AGE. El límite predeterminado es de 500 mensajes. Establecerlo en 0 desactiva por completo el borrado, que es la causa de que un catch-all con archivos adjuntos crezca de forma silenciosa. MP_MAX_AGE acepta horas o días, como 36h o 14d. Limpie el almacén durante la fase de desmontaje de CI con curl -X DELETE http://127.0.0.1:8025/api/v1/messages. Inbucket hace lo mismo con INBUCKET_STORAGE_RETENTIONPERIOD (72h) y INBUCKET_STORAGE_MAILBOXMSGCAP (300).

¿Debo ejecutar Mailpit, Inbucket o MailHog?

Use Mailpit para trabajos nuevos, a fecha de agosto de 2026. MailHog todavía funciona, pero su rama predeterminada no recibe ningún commit desde agosto de 2022, por lo que incluye dependencias sin parches. Inbucket se mantiene activamente (3.1.1, diciembre de 2025) y es la mejor opción cuando una prueba necesita POP3, ya que el servidor POP3 de Mailpit sólo se inicia después de proporcionarle un archivo de contraseñas. Mailpit usa los mismos puertos que MailHog, 1025 y 8025. Por tanto, sustituir MailHog sólo requiere cambiar el nombre de una imagen en el archivo de Compose.