How to Send Email From Self-Hosted Apps With an SMTP Relay
Set up outbound email for self-hosted apps without turning your VPS into a fragile mail server that gets blocked, throttled, or ignored.
How to choose an SMTP relay, store credentials safely, test delivery, and troubleshoot the most common mail setup failures.
Password resets, invite emails, alerts, contact forms, and transactional messages from apps on a VPS.
Running your own full mail stack sounds empowering until you hit IP reputation, SPF, DKIM, DMARC, and port-blocking problems all at once.
Before you begin
- A self-hosted app that supports SMTP settings.
- A domain you control, or at least a sender address you are allowed to use.
- An SMTP relay account, such as Mailgun, Postmark, Brevo, or another provider you trust.
- Access to your DNS provider for SPF and DKIM records if the relay requires them.
Most VPS hosts are not ideal places to send mail directly. Port 25 may be blocked, the IP may have poor reputation, and correct email authentication takes real work. An SMTP relay offloads the hardest part so your app can focus on sending a message and the relay can focus on getting it delivered.
Free or low-cost starting points vary over time, but the pattern stays the same: use a relay instead of a do-it-yourself mail server unless email infrastructure itself is your project.
Step 1: Choose a relay that fits the job
For simple app notifications, the decision usually comes down to price, free tier, setup friction, and reporting. Whatever provider you choose, collect these values:
- SMTP host, such as
smtp.postmarkapp.com - SMTP port, often
587 - Username
- Password or API key
- Encryption type, usually STARTTLS or TLS
- Verified sender domain or sender address
Also note whether the provider expects SPF and DKIM records before it trusts your domain.
Step 2: Store SMTP credentials safely
Do not hardcode SMTP passwords into version-controlled Compose files. Put them in a local env file or secret file instead.
Example .env entries:
SMTP_HOST=smtp.example-relay.com
SMTP_PORT=587
SMTP_USER=relay-user
SMTP_PASSWORD=super-secret-password
SMTP_FROM=alerts@example.com
SMTP_SECURE=falseIf your app supports file-based secrets, use those. Otherwise, keep the env file private and off Git.
Step 3: Configure the app or Compose service
Every app names its SMTP settings a little differently, but a Compose example often looks like this:
services:
app:
image: yourorg/app:1.0.0
env_file:
- .env
environment:
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM: ${SMTP_FROM}
SMTP_SECURE: ${SMTP_SECURE}Then redeploy the app:
docker compose config
docker compose up -dIf your app has an admin UI for mail settings, enter the same values there instead. Prefer app-specific documentation over guessing field names.
Expected outcome: The app can send password resets or alerts through the relay without trying to deliver mail directly from the VPS.
Step 4: Test delivery and finish the DNS side
Use the app's built-in test email feature if it has one. If it does not, trigger a real event such as a password reset.
Then check:
- The app logs for SMTP handshake or authentication errors.
- The relay dashboard for accepted, bounced, or blocked messages.
- Your DNS records for SPF and DKIM alignment.
Many providers ask you to publish DNS records like these:
example.com. TXT "v=spf1 include:relay.example.net ~all"
default._domainkey TXT "k=rsa; p=MIIBIjANBgkq..."Recovery notes:
- If a new SMTP credential fails, roll back to the old one before changing other variables at the same time.
- If you changed the sender domain, give DNS time to propagate before assuming the relay is broken.
- If email is mission-critical, keep a second relay provider documented as a fallback option.
Troubleshooting common email failures
Authentication failed.
Re-check the username, password, and whether the provider expects an API token instead of a mailbox password.
The app says it sent mail, but nothing arrived.
Look in the relay dashboard for suppression, spam, bounce, or sender-verification issues. The message may have left the app successfully but been rejected later.
TLS or certificate errors appear.
Confirm whether the provider expects port 465 with implicit TLS or port 587 with STARTTLS. Mixing those settings is a common mistake.
Messages go to spam.
Check SPF, DKIM, DMARC, sender reputation, and whether the From address matches the verified sending domain.
What to do next
Once outbound email is reliable, the next useful building block is a dependable cache or queue backend for your app stack. Continue with How to Run Redis in Docker Compose With Persistence and Auth.
