Are you still relying on limited third‑party SMTP services to test your email features? Hosted services often impose monthly sending caps, throttle rates, and require external network access. Self‑hosting MailHog in Docker eliminates these constraints, offers complete control over your local email pipeline, and reduces exposure to external dependencies. Unlike freemium platforms such as Mailtrap or SendGrid’s free tier, MailHog runs entirely on your machine at zero cost and without registration.
Using MailHog with Docker
MailHog is distributed as a lightweight Docker image. You can isolate one container per application to prevent mixing email logs across projects. By default, MailHog stores messages in memory, so every restart clears your inbox. To persist emails across restarts, configure the MH_STORAGE
and MH_MAILDIR_PATH
environment variables in your Docker Compose file and mount a volume:
mailhog:
image: mailhog/mailhog
container_name: mailhog
restart: unless-stopped
environment:
MH_STORAGE: maildir # switch from in-memory to maildir on disk :contentReference[oaicite:0]{index=0}
MH_MAILDIR_PATH: /mailhog/data # where on the container to store the maildir
volumes:
- ./mailhog_data:/mailhog/data
ports:
- '1025:1025' # SMTP
- '8025:8025' # Web UI
This creates a mailhog_data
folder at your project root. Add it to .gitignore
to avoid committing email logs.
If you prefer a single Docker command instead of Compose:
docker run -d \
--name mailhog \
--restart unless-stopped \
-e MH_STORAGE=maildir \
-e MH_MAILDIR_PATH=/mailhog/data \
-v "$(pwd)/mailhog_data:/mailhog/data" \
-p 1025:1025 \
-p 8025:8025 \
mailhog/mailhog
Access it using http://localhost:8025/
Simulating Failures with Jim
MailHog includes an optional chaos‑testing feature called Jim. Jim helps you test how your application handles intermittent SMTP issues: delayed deliveries, random drops, temporary network errors, and more. With Jim enabled you can verify retry logic, error handling, and user notifications under real‑world failure scenarios.
Integrating MailHog with NestJS
Below is an example of configuring Nest’s MailerModule to use MailHog. No authentication is required by default. Adjust config.smtpMailHost
and related variables to your environment variables.
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';
import { AppConfigModule } from '../config/app-config.module';
import { AppEnvConfigService } from '../config/environment-variables/app-env.config';
import { MailService } from 'src/application/mail/mail.service';
export function getTemplateDir(): string {
const srcDir = join(
process.cwd(),
'src',
'infrastructure',
'mail',
'templates',
);
const distDir = join(__dirname, 'templates');
return process.env.NODE_ENV === 'production' ? distDir : srcDir;
}
@Module({
imports: [
MailerModule.forRootAsync({
imports: [AppConfigModule],
inject: [AppEnvConfigService],
useFactory: (config: AppEnvConfigService) => ({
transport: {
host: config.smtpMailHost,
port: config.smtpMailPort,
secure: config.smtpMailSecure ?? false, // optional TLS toggle
auth: config.smtpMailUser
? {
// if you need auth:
user: config.smtpMailUser,
pass: config.smtpMailPassword,
}
: undefined,
},
defaults: {
from: config.smtpMailFrom,
},
template: {
dir: getTemplateDir(),
adapter: new HandlebarsAdapter(),
options: { strict: true },
},
}),
}),
],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}
Testing MailHog Setup
For a complete example of health checks and automated tests for your MailHog setup, check them out here in this repository.
How about you? Have you tried MailHog or similar containerized SMTP? Share your experience and any tips in the comments!
Top comments (0)