Deploy MailCatcher NG | (Just Updated) Email Testing SMTP Server with REST API, Login Preset, Data Survives Redeploys
MailCatcher NG email testing: SMTP inbox, web UI and REST API
mailcatcher
Just deployed
/data
Deploy and Host MailCatcher NG on Railway
Built for an LLM agent to deploy and use. There is no setup page to open: the web UI login is created from a generated password on first boot, and the contract below lists every address, variable name and one script that sends a message over SMTP, reads it back through the REST API, pulls the one-time code out of it and cleans up.
MailCatcher NG is an open source email testing server. Your application sends mail to its SMTP port instead of a real provider, and every message lands in a web inbox and a REST API with HTML and text bodies, headers and attachments. Its plugin API finds messages by search and extracts verification codes, magic links and password reset tokens, which is what an automated test or an agent needs.
About Hosting MailCatcher NG
This template runs one service: MailCatcher NG 1.7.0 in a small wrapper image, pinned by digest, with a volume on /data. The volume holds the SQLite message store, so the inbox survives redeploys. MailCatcher has no login of its own, so a Caddy reverse proxy in the same container puts HTTP Basic auth in front of the web UI and REST API as the user admin; only /healthz is open. The password is generated per deploy.
The wrapper also fixes a crash in MailCatcher NG 1.7.0: any unrecognised SMTP command, such as a port scan or a client probing VRFY, raised an ArgumentError and killed the whole process. The container now answers 500 Unknown command and keeps running.
The SMTP listener (port 1025, no login) is reachable on Railway's private network only (mailcatcher.railway.internal:1025), which is where test mail comes from: services in the same project. It is not exposed to the internet, because Railway cannot give one service both an HTTP healthcheck and a public TCP proxy. The web UI is served on Railway's injected PORT, and the newest 5000 messages are kept.
Agent contract
- Public URL:
MAILCATCHER_PUBLIC_URL(https://plus the service'sRAILWAY_PUBLIC_DOMAIN). The web UI is at/, the REST API at/messagesand under/plugin/. - Private URLs: web and API
http://mailcatcher.railway.internal:8080(MAILCATCHER_PRIVATE_URL); SMTP hostMAILCATCHER_SMTP_HOST(mailcatcher.railway.internal), port1025, plain text, no login. - Credentials:
MAILCATCHER_UI_PASSWORD(useradmin, web UI and API). SMTP needs none. - Auth: the API takes HTTP Basic auth (
curl -u admin:$MAILCATCHER_UI_PASSWORD). SMTP clients connect with TLS off. - Health:
GET /healthzreturns 200 without credentials. - API:
GET /messages,POST /plugin/search?query=...,GET /plugin/message/{id}/tokens,DELETE /plugin/messages.
# run from any service in the project that has curl, with these variables set:
# MAILCATCHER_PRIVATE_URL MAILCATCHER_SMTP_HOST MAILCATCHER_UI_PASSWORD
U="$MAILCATCHER_PRIVATE_URL"; A="admin:$MAILCATCHER_UI_PASSWORD"
curl -s -o /dev/null -w "healthz: %{http_code}\n" "$U/healthz"
curl -s -o /dev/null -w "no auth: %{http_code}\n" "$U/messages"
printf 'From: app@example.com\r\nTo: user@example.com\r\nSubject: hello from an agent\r\n\r\nYour code is 482913\r\n' | curl -s --url "smtp://$MAILCATCHER_SMTP_HOST:1025" --mail-from app@example.com --mail-rcpt user@example.com -T -; echo "smtp exit: $?"
curl -s -u "$A" "$U/messages"; echo
ID=$(curl -s -u "$A" "$U/messages" | sed -n 's/.*"id":\([0-9]*\).*/\1/p' | tail -1); echo "id: $ID"
curl -s -w " [%{http_code}]\n" -u "$A" "$U/plugin/message/$ID/tokens"
curl -s -w " [%{http_code}]\n" -u "$A" -X POST "$U/plugin/search?query=hello"
curl -s -u "$A" -X DELETE -o /dev/null -w "delete: %{http_code}\n" "$U/plugin/messages"
A fresh deploy prints healthz: 200, no auth: 401, smtp exit: 0, a JSON list holding the new message, its id, a tokens object containing the code 482913 with [200], a search result with "count":1 and [200], and delete: 204.
To use it from your own service, point its SMTP settings at host MAILCATCHER_SMTP_HOST, port 1025, no authentication and no TLS, then read the result back with the same API calls.
Why Deploy MailCatcher NG on Railway?
Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.
By deploying MailCatcher NG on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.
- Agent-ready - a generated login, a contract block and one working script; nothing to type into the deploy form.
- Locked from boot - the inbox needs a login, so test mail with real-looking content is not public, and SMTP stays on the private network.
- Verification codes as an API - the tokens endpoint returns the one-time codes, magic links and reset tokens found in a message.
- State survives redeploys - the message store lives on a volume, and the image is pinned by digest.
- Newer and sturdier than the public image - version 1.7.0 against 1.6.8 on Docker Hub, with the unknown-command crash fixed.
Common Use Cases
- Staging mail catcher - point a staging app's SMTP settings at
MAILCATCHER_SMTP_HOSTand read password resets and order emails in the web UI. - End-to-end sign-up tests - a test run registers a user, searches for the message and reads the one-time code from the tokens endpoint.
- Agent email checks - an AI agent verifies that the mail its code sends arrives with the right recipients, subject and links, and deletes it afterwards.
Dependencies for MailCatcher NG Hosting
- One Railway volume on
/data, created by the template.
Deployment Dependencies
Implementation Details
| Variable | Purpose |
|---|---|
MAILCATCHER_UI_PASSWORD | Password of the admin user for the web UI and REST API. Generated per deploy. |
MAILCATCHER_PUBLIC_URL | HTTPS address of the web UI and API. |
MAILCATCHER_PRIVATE_URL | Web UI and API address on the private network. |
MAILCATCHER_SMTP_HOST | Private-network host name of the SMTP listener (port 1025). |
The image entrypoint hashes the password into a Caddy basic-auth config, starts MailCatcher on the loopback with persistence under /data, and puts Caddy on PORT in front of it.
Template Content
mailcatcher
ghcr.io/bon5co/mailcatcher:1.7.0