Sign inSign up

mailtrap/mailtrap-local

By mailtrap

•Updated about 2 months ago

Open-source local email sandbox: SMTP + web UI + API in one binary. Catch every email you send.

Image
Developer tools
0

6.2K

mailtrap/mailtrap-local repository overview

⁠Mailtrap Local — a local email sandbox for developers

CI License: MIT GitHub release

GitHub⁠ · Docs⁠ · API⁠ · Mailtrap.io⁠

Mailtrap Local is an open-source SMTP server you run on your own machine. It catches every email your app sends, stores them in a local SQLite database, and gives you a web sandbox to browse, search, and inspect them in. Nothing leaves your machine unless you tell it to.

One self-contained binary in a distroless image — under 7 MB, linux/amd64 + linux/arm64, MIT-licensed.

Mailtrap Local inbox with HTML preview

⁠Features

  • SMTP server on 3535 — point any app, framework, or test suite at it. Anonymous SMTP — no authentication, no TLS, nothing to configure but the host and port.

  • Web sandbox on 3550 — search caught mail and inspect rendered HTML, plain text, raw source, headers, and attachments.

  • REST API on 3550 — documented with an OpenAPI 3.1 spec, served at /api/v1/openapi.yaml.

  • HTML check — score an email's compatibility across major mail clients, weighted by real client market share.

  • Send to Cloud — promote any caught message to your hosted Mailtrap⁠ sandbox in one click, so a designer or PM can review what your local code produced without shell access.

  • Relay and auto-relay — release a caught message through any upstream SMTP server (Mailtrap, SendGrid, Mailgun, SES, Postmark) when you need it to land in a real inbox.

  • Outbound webhooks — HMAC-signed POST on every caught message, so your app can react in tests.

  • Categories — label mail at send time with X-MT-Category (the same header convention as Mailtrap cloud) and filter on it in the UI and API.

  • Live updates — new mail appears over a WebSocket, no refresh.

  • Automatic pruning — a configurable retention cap evicts the oldest messages.

HTML Check tab with market support and issue breakdown

⁠Quick start

docker run -d \
  --name mailtrap-local \
  --restart unless-stopped \
  -p 3535:3535 \
  -p 3550:3550 \
  -v mailtrap-local:/var/lib/mailtrap-local \
  mailtrap/mailtrap-local:latest

Open http://127.0.0.1:3550⁠ for the sandbox, and point your app's SMTP client at 127.0.0.1:3535.

PortPurpose
3535SMTP ingest
3550Web sandbox + REST API + WebSocket

⁠Docker Compose

services:
  mailtrap-local:
    image: mailtrap/mailtrap-local:latest
    restart: unless-stopped
    ports:
      - "3535:3535"
      - "3550:3550"
    volumes:
      - mailtrap-local:/var/lib/mailtrap-local

volumes:
  mailtrap-local:

Other services on the same Compose network reach the SMTP server at mailtrap-local:3535.

⁠Persisting caught mail

The image declares a volume at /var/lib/mailtrap-local. It holds two files that belong together:

  • db.sqlite3 — every caught message
  • secret.key — encrypts the relay, cloud, and webhook credentials stored in that database

Mount the whole directory, not just the database. The image pins MAILTRAP_LOCAL_SECRET_KEY_FILE onto the volume for exactly this reason: if the key lived on the container filesystem, a docker pull upgrade would generate a fresh one and silently break every saved connection.

⁠Runtime options

The image's default command already sets the flags a container needs:

FlagValue in the image
--http-listen0.0.0.0:3550
--smtp-listen0.0.0.0:3535
--db/var/lib/mailtrap-local/db.sqlite3
--unsafe-non-loopbackset

If you override the command, keep --unsafe-non-loopback and repeat the other three — Docker replaces the default command wholesale rather than merging with it. The binary refuses to bind a non-loopback address without --unsafe-non-loopback, and inside a container it has to bind 0.0.0.0 to be reachable through -p. Host exposure is still governed by your port mapping, not by the bind.

⁠Configuring cloud, relay, and webhooks

You can set these in the UI, or pin them from a YAML file mounted into the container:

docker run -d \
  -p 3535:3535 -p 3550:3550 \
  -v mailtrap-local:/var/lib/mailtrap-local \
  -v "$PWD/config.yml:/etc/mailtrap-local/config.yml:ro" \
  -e MAILTRAP_LOCAL_CONFIG=/etc/mailtrap-local/config.yml \
  -e MAILTRAP_API_TOKEN=... \
  mailtrap/mailtrap-local:latest
storage:
  max_messages: 500
cloud:
  api_token: ${MAILTRAP_API_TOKEN}
  sandbox_id: 1234567
relay:
  host: smtp.example.com     # your upstream SMTP host
  port: 587
  username: ${SMTP_USERNAME}
  password: ${SMTP_PASSWORD}
  auto_relay_enabled: false
webhook:
  url: http://host.docker.internal:4000/hooks/mail
  secret: ${WEBHOOK_SECRET}
  enabled: true

Values support ${ENV_VAR} interpolation. Any key present in the file overrides the equivalent value in the database and becomes read-only in the UI — which is what you want for a container you rebuild often.

⁠Tags

TagUse it for
latestthe most recent release
0.4.1pinning a release — recommended in CI
0.4.1-amd64, 0.4.1-arm64single-arch pulls; rarely needed, since latest and the version tags are manifest lists

The same images are mirrored to ghcr.io/mailtrap/mailtrap-local.

⁠What it is not

Mailtrap Local is a tool for one developer on one machine. There are no user accounts, no authentication, no multi-tenancy, no auth on the local API, and no RBAC. Storage is SQLite only.

If you need shared inboxes, team access, and a hosted setup, use Mailtrap⁠ — the hosted product this complements. Any message you catch locally can be pushed there with Send to Cloud.

⁠Other ways to install

Homebrew, Snap, Nix, and prebuilt binaries for macOS and Linux — see the README⁠.

⁠License

MIT — source on GitHub⁠. Issues and pull requests welcome.

Tag summary

Content type

Image

Digest

sha256:07467e8b6…

Size

6.8 MB

Last updated

about 2 months ago

docker pull mailtrap/mailtrap-local