MailDev is a local SMTP server with a web UI on port 1080 that catches every email instead of delivering it. Spun up in 30 seconds via Docker, it kills the entire pain of using a real SMTP server during development: no spam to real users, no deliverability drama, no waiting on postmaster tools.
The pain with real SMTP servers in dev π€
Anyone who has ever tried to develop transactional emails against a real SMTP server knows the drill: your local IP is not in SPF, DKIM is missing, the mail lands in spam or gets bounced outright. When it does arrive, it only reaches your own test addresses, and occasionally a real customer too, because a seed address slipped through.
On top of that you get rate limits, IP reputation issues, greylisting and TLS pitfalls, all for a message you just want to look at to make sure your template renderer is not generating garbage. That is not a dev workflow, that is self-punishment.
What MailDev does π¬
MailDev is a tiny Node tool that starts two things at once: an SMTP server on port 1025 that accepts any incoming mail, and a web UI on port 1080 where you can inspect each message as HTML, plain text, headers or attachments. New mails pop in live via WebSocket, no refresh needed.
- SMTP on port 1025, web UI on port 1080 (freely configurable)
- HTML, text and header views plus attachments
- REST API to read and delete mails (perfect for E2E tests)
- Optional relay to a real upstream SMTP server when you do want to send out
- WebSocket live updates, no polling required
- Optional Basic Auth for the web UI, optional HTTPS
Up and running in 30 seconds π
Fastest path is Docker, without touching npm in your project:
docker run -p 1080:1080 -p 1025:1025 maildev/maildev:2.2.1Open http://localhost:1080 and start sending mail to localhost:1025. If you would rather avoid Docker, the npm package works just as well:
npm install -g maildev
maildevDocker Compose for your dev setup π³
In a real dev workflow MailDev makes the most sense as a dedicated service next to your API. Nothing globally installed, and your whole team gets the same email sandbox just by running docker compose up.
services:
maildev:
image: maildev/maildev:2.2.1
container_name: maildev
restart: unless-stopped
ports:
- "1025:1025" # SMTP
- "1080:1080" # Web UI
environment:
- MAILDEV_INCOMING_USER=
- MAILDEV_INCOMING_PASS=
- TZ=Europe/Berlin
api:
build: ./api
environment:
SMTP_HOST: maildev
SMTP_PORT: 1025
SMTP_FROM: "Dev API <[email protected]>"
depends_on: [maildev]Example: Node.js + Nodemailer π¨
The classic. Nodemailer does not know about MailDev specifically because MailDev is just a normal SMTP server. You point it at the MailDev host, disable TLS for local dev, done.
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "localhost",
port: 1025,
secure: false, // MailDev macht kein TLS in dev
ignoreTLS: true,
// auth bewusst weglassen, MailDev nimmt alles
});
await transporter.sendMail({
from: '"Dev Bot" <[email protected]>',
to: "[email protected]",
subject: "Hallo aus dev",
html: "<h1>Funktioniert</h1><p>Schau in MailDev :)</p>",
});Example: NestJS mailer module πͺΊ
In a Nest app, the cleanest path is the official @nestjs-modules/mailer module, pointed at MailDev via ENV. In dev/test everything flows through MailDev, in prod SMTP_HOST simply points at the real server, no code change.
// app.module.ts
import { MailerModule } from "@nestjs-modules/mailer";
@Module({
imports: [
MailerModule.forRoot({
transport: {
host: process.env.SMTP_HOST ?? "localhost",
port: Number(process.env.SMTP_PORT ?? 1025),
secure: false,
ignoreTLS: true,
},
defaults: { from: '"NestJS Dev" <[email protected]>' },
}),
],
})
export class AppModule {}Example: Nuxt 3 server route β‘
Nuxt 3 ships with Nitro, a fully featured server layer. An API route that sends a mail is three lines of code, and thanks to MailDev you can test it end-to-end without ever letting a message leave your laptop.
// server/api/welcome.post.ts
import nodemailer from "nodemailer";
export default defineEventHandler(async (event) => {
const body = await readBody<{ email: string }>(event);
const t = nodemailer.createTransport({
host: process.env.SMTP_HOST ?? "localhost",
port: Number(process.env.SMTP_PORT ?? 1025),
secure: false,
ignoreTLS: true,
});
await t.sendMail({
from: '"Nuxt" <[email protected]>',
to: body.email,
subject: "Welcome",
html: "<p>SchΓΆn, dass du da bist.</p>",
});
return { ok: true };
});Alternatives: Mailpit and Mailhog βοΈ
MailDev is not the only tool in this category. Two prominent alternatives are worth knowing:
- Mailpit (Go, spiritual successor to Mailhog): actively maintained, adds spam scoring, link checking and a chaos mode for bounce simulation. If you want "more batteries included", Mailpit is a strong option.
- Mailhog (Go, older): in maintenance mode for years, many tutorials still show it. It works, but I would not build new setups on it.
- MailDev (Node): minimal, very stable, feels the most natural for Node projects. If you already have a Node stack, this gives you the least friction.
Wiring it into your dev workflow π
My per-project setup looks like this:
- MailDev runs as a service in docker-compose.yml, always pinned to a fixed tag (no :latest).
- All SMTP config in the app comes from ENV (SMTP_HOST, SMTP_PORT, SMTP_FROM). Dev points at maildev, prod points at the real server.
- E2E tests call the REST API DELETE /email/all at the start, then trigger the action, then fetch the mail via GET /email to assert on subject and HTML.
- Onboarding doc says exactly: docker compose up, then open http://localhost:1080. Nobody has to share SMTP credentials.
- Production SMTP credentials only live in the secret manager, never in the .env example file.
The mail pipeline is now fully deterministic locally. No spam score, no reputation risk, no "wait, was that last mail from local or from staging?" moments.
Verdict π
MailDev is one of those tools you set up once and never touch again because it just runs. One container, two ports, every mail your app sends safely captured, and a web UI you can still operate at 3am. For every stack that sends mail anywhere, this should be the default answer to "how do we test this locally?".
Skip the pain with real SMTP servers until you actually deploy to production. Until then: docker compose up.