Test email in Docker and CI with a disposable inbox
Mailhog proves an email was sent, not delivered. Test real email from a Docker container or CI job with a disposable inbox over HTTP — create, wait, assert.
Mailhog proves an email was sent, not delivered. Test real email from a Docker container or CI job with a disposable inbox over HTTP — create, wait, assert.
Rather than running complex SMTP catchers inside Docker or CI, test end-to-end email flows using disposable inboxes over HTTP. Provision isolated mailboxes via API, trigger verification emails, long-poll for message arrival, and assert on extracted OTPs or magic links.
- Verify Real Delivery: Local SMTP mocks only test dispatch, while disposable HTTP inboxes validate actual end-to-end delivery and authentication.
- Zero Docker Plumbing: No need to run mock SMTP containers or expose internal ports; tests communicate over plain HTTPS with an API token.
- Eliminate Flaky Sleeps: Built-in long-polling wait endpoints return immediately when mail arrives.
- Parallel Test Isolation: Assign unique inboxes per CI worker to prevent race conditions and mailbox crosstalk.
You containerized your app and now your email tests are awkward: a local SMTP catcher (Mailhog, Mailpit) runs as another service and only proves your app tried to send. To test that email actually delivers and contains the right thing, use a real disposable inbox over HTTP — no SMTP service in your compose file.
Why local SMTP catchers fall short
Mailhog and Mailpit are great for seeing outgoing mail in development. But they intercept SMTP locally, so they can’t tell you whether mail actually:
- delivers to a real inbox,
- passes authentication (SPF/DKIM/DMARC),
- renders the right code or link for a recipient.
For a test that proves the real round-trip, you need a real inbox your container can read over HTTP.
The pattern in a container
Pass an API token as an env var; everything else is HTTP, so there’s nothing to add to docker-compose.yml:
# docker-compose.yml (test)
services:
app:
build: .
environment:
- OLLASTACK_API_TOKEN=${OLLASTACK_API_TOKEN}
Inside the test (any language — Node shown):
const API = "https://login.ollastack.com";
const h = { Authorization: `Bearer ${process.env.OLLASTACK_API_TOKEN}` };
// 1. disposable inbox
const inbox = await fetch(`${API}/api/mailboxes`, {
method: "POST", headers: { ...h, "Content-Type": "application/json" },
body: JSON.stringify({ name: "ci", mode: "test" }),
}).then(r => r.json());
// 2. drive your app with inbox.address (sign up, reset, etc.)
await signup(inbox.address);
// 3. long-poll for the email and assert
const msg = await fetch(`${API}/api/mailboxes/${inbox.id}/wait?timeout=60`, { headers: h })
.then(r => r.json());
expect(msg.codes[0]).toMatch(/^\d{6}$/);
No SMTP container, no port wiring — just the token. The wait endpoint long-polls, so the test isn’t a flaky sleep, and codes[]/links[] are extracted so you don’t scrape HTML.
In GitHub Actions / GitLab CI
It’s the same HTTP calls from a CI runner — add OLLASTACK_API_TOKEN as a secret:
# .github/workflows/ci.yml
jobs:
test:
runs-on: ubuntu-latest
env:
OLLASTACK_API_TOKEN: ${{ secrets.OLLASTACK_API_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
No service container, no SMTP sink to maintain.
Keep it isolated
- One inbox per test (or a
+tagsubaddress) so parallel jobs don’t read each other’s mail. - Bulk-clear between runs and set a retention window so test mail purges itself.
- Test inboxes are never spam-filtered, so a strict transactional email is never dropped from under your assertion.
Frequently Asked Questions
How do I test email from inside a Docker container?
Use a disposable inbox over HTTP rather than a local SMTP catcher: create an inbox via the API, point your app at its address, then long-poll the wait endpoint and assert on the extracted code or link. It needs only an OLLASTACK_API_TOKEN env var — no SMTP service in the compose file.
Why not just use Mailhog or Mailpit?
Mailhog/Mailpit catch SMTP locally, which proves your app tried to send — but not that mail actually delivers, authenticates, or renders for a real recipient. A real disposable inbox tests the true round-trip.
Does it work in GitHub Actions or GitLab CI?
Yes — it's plain HTTP, so it runs from any container or CI runner with just the API token as a secret. No service container to stand up.
Wrapping Up
In Docker and CI environments, skip the local SMTP catcher when you need to validate full email deliverability. A disposable inbox over HTTP gives you end-to-end verification of deliverability, authentication headers, and payload content with a single API token.
Isolate parallel tests by provisioning unique inboxes or subaddresses per worker, and use automatic retention policies to keep CI environments clean.