Email-driven login flows are easy to break locally. You click Continue with email, the app says it sent a link, and then nothing useful happens unless you have SMTP configured.

MailDev gives you a development inbox for that exact moment. Point your app at MailDev’s SMTP port, open the web UI, and inspect the message before it ever reaches a real mailbox.

MailDev is a local SMTP server and browser inbox for testing generated emails during development.

What is MailDev?

MailDev catches outgoing email from your application and shows it in a browser UI. It is useful for magic links, password resets, signup emails, notifications, transactional templates, and any workflow where you want to prove the app generated the right message without sending mail to real users.

The default ports are:

  • 1025 for SMTP
  • 1080 for the web UI and REST API

For a host app, configure SMTP like this:

SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false

For an app running in the same Docker Compose network, use the service name:

SMTP_HOST=maildev
SMTP_PORT=1025
SMTP_SECURE=false

Why Use MailDev?

MailDev is a good fit when you need a no-drama email catcher in a development or homelab test environment:

  • Magic-link testing: receive login and verification links locally.
  • Template inspection: check rendered HTML, plain text, headers, raw source, and attachments.
  • Responsive preview: resize the email preview to catch mobile layout issues.
  • No accidental delivery: keep test messages away from real inboxes.
  • API access: query captured emails from integration tests or scripts.
  • Relay support: optionally forward selected messages through a real SMTP server when you explicitly configure it.
  • Agent workflows: MailDev 3 can expose an MCP endpoint for development agents that need to inspect verification emails.

Do not run MailDev as your production mail server. It is a local testing tool.

Tech Overview of MailDev

MailDev 3 is a TypeScript monorepo. The current project layout separates the SMTP server, API server, UI, shared storage/types, CLI, and MCP integration into packages.

The web interface is built with React. The API layer uses Fastify, real-time updates are delivered over WebSockets, and the SMTP parsing stack builds on the Nodemailer ecosystem.

At runtime, the flow is simple:

Your app -> SMTP maildev:1025 -> MailDev storage -> Web UI/API at :1080

If you enable persistence with MAILDEV_MAIL_DIRECTORY, messages survive container restarts. If you leave the retention limit at MailDev’s default, it keeps every message, so a long-running setup should set MAILDEV_MAX_EMAILS.

Self-Hosting MailDev with Docker

The Home-Lab compose file keeps MailDev local by default and caps the inbox at 1000 messages.

Home-Lab MailDev compose reference
services:
  maildev:
    image: maildev/maildev:latest
    container_name: maildev
    restart: unless-stopped
    ports:
      - "${MAILDEV_WEB_PORT:-127.0.0.1:1080}:1080"
      - "${MAILDEV_SMTP_PORT:-127.0.0.1:1025}:1025"
    environment:
      TZ: ${TZ:-Europe/Warsaw}
      MAILDEV_WEB_PORT: 1080
      MAILDEV_SMTP_PORT: 1025
      MAILDEV_WEB_IP: 0.0.0.0
      MAILDEV_IP: 0.0.0.0
      MAILDEV_MAX_EMAILS: ${MAILDEV_MAX_EMAILS:-1000}
      MAILDEV_MAX_MESSAGE_SIZE: ${MAILDEV_MAX_MESSAGE_SIZE:-52428800}

Start it:

cp .env.sample .env
docker compose up -d

Then open:

http://localhost:1080

Point a host app at:

SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false

Or, if your app is another service in the same Compose project:

SMTP_HOST=maildev
SMTP_PORT=1025
SMTP_SECURE=false

Testing a Message

From a container on the same Docker network, you can send a tiny smoke-test message:

docker run --rm --network maildev_default alpine:3.20 sh -c '
  apk add --no-cache busybox-extras >/dev/null &&
  {
    sleep 1;
    printf "HELO local.test\r\n";
    sleep 1;
    printf "MAIL FROM:<[email protected]>\r\n";
    sleep 1;
    printf "RCPT TO:<[email protected]>\r\n";
    sleep 1;
    printf "DATA\r\n";
    sleep 1;
    printf "Subject: MailDev smoke test\r\n";
    printf "From: [email protected]\r\n";
    printf "To: [email protected]\r\n";
    printf "\r\n";
    printf "Hello from Docker Compose.\r\n";
    sleep 1;
    printf ".\r\n";
    sleep 1;
    printf "QUIT\r\n";
  } | nc maildev 1025
'

Then refresh http://localhost:1080 and the message should be visible.

You can also query the REST API:

curl http://localhost:1080/api/email/summary

That makes MailDev useful for automated tests that need to confirm a signup, invite, or reset email was generated.

Using MailDev with Self-Hosted Apps

MailDev works well with apps that require SMTP but do not need real delivery during testing:

  • Rallly magic-link login
  • Listmonk campaign template checks
  • Mautic form and automation testing
  • Ghost membership email previews
  • Any app with password reset or invite emails

The important Docker rule is the same as with Mailpit: if the app and MailDev run in the same Compose network, the SMTP host is maildev, not localhost.

Safe Exposure Notes

Keep MailDev private. Captured messages often include login links, password reset URLs, invite tokens, internal hostnames, and test user data.

For local use, bind the ports to loopback:

ports:
  - "127.0.0.1:1080:1080"
  - "127.0.0.1:1025:1025"

If you expose MailDev on a LAN or behind a tunnel, configure authentication first and remember that the inbox content is sensitive.

Conclusion

MailDev is a practical local inbox for development email. It is easy to run with Docker, simple to wire into most apps, and useful when testing magic links or transactional templates without configuring a real SMTP provider.

If you already use Mailpit, MailDev is not a mandatory replacement. Think of it as another good local email catcher, especially interesting now that MailDev 3 has a refreshed TypeScript/React stack and MCP support.

FAQ