Docker & Docker Compose
Run MailDev as a container, wire it into a Compose stack so your app can reach it by service name, pass CLI flags and environment variables, and use the built-in health check.
The official image is maildev/maildev
on Docker Hub. It is built on node:22-alpine, runs as the unprivileged node
user, and exposes the same two ports as the npm package.
Run it #
docker run -p 1080:1080 -p 1025:1025 maildev/maildev
Give it a name so you can stop and start it again:
docker run -d --name maildev -p 1080:1080 -p 1025:1025 maildev/maildev
docker stop maildev
docker start maildev
Passing flags #
The image's entrypoint is the MailDev binary, so anything after the image name goes straight to the CLI:
docker run -p 1080:1080 -p 1025:1025 maildev/maildev --base-pathname /maildev --max-emails 200
Environment variables #
Every CLI flag has an environment-variable equivalent, which is usually the better fit for a container. The image sets three by default:
| Variable | Default | Meaning |
|---|---|---|
MAILDEV_WEB_PORT |
1080 |
Web UI, REST API, and MCP endpoint |
MAILDEV_SMTP_PORT |
1025 |
SMTP listener |
TZ |
UTC |
Timezone used for displayed timestamps |
Set TZ if you want received timestamps in your own timezone:
docker run -p 1080:1080 -p 1025:1025 -e TZ=Europe/Berlin maildev/maildev
The CLI reference lists every other variable.
Docker Compose #
The important part is that your app must reach MailDev by service name, not
localhost. Inside a container, localhost is that container.
services:
maildev:
image: maildev/maildev
ports:
- '1080:1080'
# The SMTP port does not need publishing if only other
# services in this stack send mail.
app:
build: .
environment:
SMTP_HOST: maildev
SMTP_PORT: 1025
depends_on:
maildev:
condition: service_healthy
condition: service_healthy works because the image ships a health check — see
below. Publishing 1080 but not 1025 is the shape you usually want: you need
the inbox in your browser, but only sibling containers need to send mail.
Reaching MailDev from the host #
If MailDev runs in Compose and your app runs on the host, publish the SMTP port
(- '1025:1025') and send to localhost:1025 as usual.
Reaching the host from MailDev's container #
The reverse — an app on the host, MailDev in a container — needs no special
setup, but if you configure relaying to an SMTP server on
the host, use host.docker.internal rather than localhost on Docker Desktop.
On Linux, add:
extra_hosts:
- 'host.docker.internal:host-gateway'
Health check #
The image defines its own HEALTHCHECK, so orchestrators know when MailDev is
ready:
| Setting | Value |
|---|---|
| Interval | 10s |
| Timeout | 5s |
| Start period | 5s |
| Retries | 3 |
It probes the API's /api/healthz endpoint, honors MAILDEV_BASE_PATHNAME, and
falls back to a plain TCP check on the SMTP port when the web UI is disabled with
--disable-web. You can query the same endpoint yourself:
docker exec maildev node dist/bin/healthcheck.js
curl http://localhost:1080/api/healthz
Persisting mail across restarts #
By default messages live in memory and vanish when the container stops. Mount a
volume and point --mail-directory at it to keep them:
services:
maildev:
image: maildev/maildev
ports:
- '1080:1080'
environment:
MAILDEV_MAIL_DIRECTORY: /home/node/mail
MAILDEV_MAX_EMAILS: '500'
volumes:
- maildev-mail:/home/node/mail
volumes:
maildev-mail:
The container runs as node (uid 1000), so the mount has to be writable by that
user. MAILDEV_MAX_EMAILS caps the store and evicts the oldest messages once the
limit is reached — worth setting on a persistent volume so a long-running stack
does not grow without bound.
Kubernetes #
Nothing about MailDev is Kubernetes-specific: one Deployment, one Service
exposing 1025 and 1080, and the same health endpoint for both probes.
readinessProbe:
httpGet:
path: /api/healthz
port: 1080
livenessProbe:
httpGet:
path: /api/healthz
port: 1080
MailDev has no authentication by default and accepts mail from anyone who can
reach the port. Keep it inside your cluster or development network — never behind
a public ingress. If you must expose the UI, put
basic auth on it with --web-user and --web-pass.