You deploy your app to a fresh Droplet. Everything works: the pages load, the database answers, the health check is green. Then a user asks for a password reset, and nothing arrives. Your logs show the mail library waiting, and waiting, and finally giving up with ETIMEDOUT.
Often nothing is wrong with your code: DigitalOcean blocks outbound SMTP on Droplets. Their documentation says it plainly:
SMTP ports 25, 465, and 587 are blocked on Droplets to prevent spam and other abuses on our platform.
Port 25 was blocked first. On 6 March 2025 DigitalOcean added ports 465 and 587, which closed the usual workaround of sending through a provider on the submission ports. This post shows how to confirm that the block is your problem, and two ways to send email from a Droplet anyway.
TL;DR
- DigitalOcean blocks outbound SMTP ports 25, 465 and 587 on Droplets, including traffic through a Reserved IP. Their documentation describes no way to request an exception.
- DigitalOcean also recommends against running your own mail server and points to a third-party email service instead.
- Option 1, the best one: send through an HTTPS API on port 443, which the SMTP block does not touch.
- Option 2, for apps that only speak SMTP: use a provider that accepts SMTP on port 2525. SMTPfast does, with STARTTLS.
- Test from the Droplet itself before you change anything. Some accounts are not affected, ours included.
Is the block your problem? Test it from the Droplet
Before you change your mail setup, check what the Droplet can actually reach. A filtered port usually does not answer with an error: the connection attempt never completes, which is why mail libraries hang until their timeout.
A quick check with nc, run on the Droplet:
# -z: only test the connection, -v: say what happened, -w 5: give up after 5 seconds
nc -zv -w 5 smtp.smtpfa.st 587
nc -zv -w 5 smtp.smtpfa.st 2525
If port 587 times out while port 2525 connects, that is consistent with the SMTP block. A timeout can also come from your own firewall (a DigitalOcean Cloud Firewall or ufw with outbound rules), so check those, and look at the pattern: with the block, every SMTP destination on 25, 465 and 587 times out, while HTTPS to a known site such as curl -I https://www.digitalocean.com works.
Not every account sees the block. We ran these tests from a fresh Droplet in FRA1 on 26 September 2026, and on our account ports 25, 465 and 587 all answered, both to Gmail and to SMTPfast. We do not know why our account is not affected; DigitalOcean's documentation describes the block as the default for all Droplets. That is exactly why you should test from your own Droplet instead of relying on a blog post, including this one.
Why DigitalOcean does this, and why they do not want you to run a mail server
Cloud IP ranges are a favourite for spammers: a new VM costs a few dollars, sends until it is caught, and gets thrown away. Every spam run from a Droplet damages the reputation of DigitalOcean's IP ranges, which hurts every honest customer on them. Blocking SMTP by default stops that at the network level.
The same page goes further than the block. DigitalOcean advises against the "just install Postfix" route entirely:
Even if SMTP were available, we strongly recommend against running your own mail server, as self-hosted mail servers are difficult to secure and maintain, frequently get flagged as spam, and require constant monitoring to protect your IP address.
and recommends the alternative in one line:
To send mail from services hosted on DigitalOcean, we recommend using a third-party email as a service provider.
This is good advice even without the block. A new IP address has no sending reputation. Gmail and Outlook are cautious with mail from fresh cloud IPs, so a self-hosted server usually starts life in the spam folder, and keeping it out means managing SPF, DKIM, DMARC, reverse DNS, bounce handling, feedback loops and blocklist monitoring yourself. Our guide on why emails go to spam covers how much of that there is.
So the question is not how to get around the block to run your own server. It is which of the two remaining doors to use.
Option 1: send over HTTPS (recommended)
An email API is plain HTTPS on port 443, the same port your app already uses for everything else, so DigitalOcean's SMTP block does not apply to it. It is also easier to debug than SMTP: one request, one response, and a clear error message when something is wrong instead of a half-finished SMTP conversation.
With SMTPfast it is a single POST:
curl -X POST https://smtpfa.st/api/v1/emails \
-H "Authorization: Bearer $SMTPFAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Reset your password",
"html": "<p>Click the link below to choose a new password.</p>"
}'
The response comes back straight away with the email's ID, for example { "id": "email_abc123" }, which you can use to follow the message in the logs.
In Node.js you need no SDK, since fetch is built in from Node 18:
const res = await fetch("https://smtpfa.st/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SMTPFAST_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "[email protected]",
to: "[email protected]",
subject: "Reset your password",
html: "<p>Click the link below to choose a new password.</p>",
}),
});
if (!res.ok) {
throw new Error(`Send failed: ${res.status} ${await res.text()}`);
}
const { id } = await res.json();
And in Python with requests:
import os
import requests
res = requests.post(
"https://smtpfa.st/api/v1/emails",
headers={"Authorization": f"Bearer {os.environ['SMTPFAST_API_KEY']}"},
json={
"from": "[email protected]",
"to": "[email protected]",
"subject": "Reset your password",
"html": "<p>Click the link below to choose a new password.</p>",
},
timeout=10,
)
res.raise_for_status()
print(res.json()["id"])
If your code already uses the Resend SDK, you do not even need to change the calls: SMTPfast accepts the same API, so switching is two lines, the base URL and the key.
Option 2: keep SMTP, change the port to 2525
Plenty of software sends through SMTP: WordPress plugins, Ghost, Django and Laravel apps configured for SMTP, Postfix relaying cron output, older applications nobody wants to touch. For those, the fix is a provider that accepts SMTP on a port DigitalOcean does not block. Port 2525 is the unofficial standard for exactly this situation.
SMTPfast's SMTP settings:
Host: smtp.smtpfa.st
Port: 2525 (587 also works where it is not blocked)
Security: STARTTLS
Username: smtpfast
Password: your SMTPfast API key (with the email:send scope)
The connection starts in plain text and upgrades to TLS with STARTTLS before any credentials are sent, the same way port 587 works. So in most libraries the setting is "port 2525, TLS on, not implicit SSL".
Here is what that looks like from a fresh Droplet (recorded on 26 September 2026, FRA1). A small Python socket check read the server greeting on port 2525:
smtp.smtpfa.st 2525 TCP connect 0.01s | 220 smtp.smtpfa.st ESMTP SMTPfast
Then openssl upgraded the connection with STARTTLS and sent EHLO over TLS. We kept only the server's reply lines:
$ printf 'EHLO test.example\r\nQUIT\r\n' \
| openssl s_client -starttls smtp -connect smtp.smtpfa.st:2525 -quiet -crlf \
| grep -E '^(250|221)'
250 HELP
250-smtp.smtpfa.st
250-SIZE 10485760
250 AUTH PLAIN LOGIN
221 Bye
The first 250 HELP ends the plain-text EHLO that openssl sends before STARTTLS; that plain-text list offers STARTTLS but no AUTH. Only after the upgrade does SMTPfast offer AUTH PLAIN LOGIN: it requires TLS before it accepts credentials. On your side, configure the client to require STARTTLS too (the examples below do), so it never sends a password on an unencrypted connection. (The -crlf flag is redundant here, because printf already sends \r\n; the command is shown as we ran it.)
Finally, the HTTPS API from the same Droplet, called without a key on purpose:
$ curl -s -o /dev/null -w "POST https://smtpfa.st/api/v1/emails -> HTTP %{http_code} in %{time_total}s\n" \
-X POST https://smtpfa.st/api/v1/emails -H "Content-Type: application/json" -d '{}'
POST https://smtpfa.st/api/v1/emails -> HTTP 401 in 0.155812s
The 401 is the API refusing a request without a key, which is exactly what you want to see from a network test: the request got through.
Node.js with Nodemailer
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "smtp.smtpfa.st",
port: 2525,
secure: false, // start in plain text...
requireTLS: true, // ...and refuse to continue without STARTTLS
auth: {
user: "smtpfast",
pass: process.env.SMTPFAST_API_KEY,
},
});
await transporter.sendMail({
from: "[email protected]",
to: "[email protected]",
subject: "Reset your password",
html: "<p>Click the link below to choose a new password.</p>",
});
secure: true is for implicit TLS (port 465). On 2525, keep it false and set requireTLS: true so a misconfigured network cannot silently downgrade you to plain text.
Django
# settings.py
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.smtpfa.st"
EMAIL_PORT = 2525
EMAIL_USE_TLS = True # STARTTLS; do not also set EMAIL_USE_SSL
EMAIL_HOST_USER = "smtpfast"
EMAIL_HOST_PASSWORD = os.environ["SMTPFAST_API_KEY"]
DEFAULT_FROM_EMAIL = "[email protected]"
Laravel
# .env
MAIL_MAILER=smtp
MAIL_HOST=smtp.smtpfa.st
MAIL_PORT=2525
MAIL_USERNAME=smtpfast
MAIL_PASSWORD=your_smtpfast_api_key
[email protected]
Laravel's mailer (Symfony Mailer) upgrades to TLS with STARTTLS automatically when the server offers it (which SMTPfast does on port 2525), as long as PHP's OpenSSL extension is enabled and automatic TLS has not been turned off. SMTPfast refuses to accept credentials without TLS, so a connection that did not upgrade fails instead of sending your key in the clear.
Postfix, for cron and system mail
If you run Postfix on the Droplet only so that cron jobs, fail2ban and unattended upgrades can email you, turn it into a relay instead of a mail server. It hands everything to SMTPfast on port 2525:
# /etc/postfix/main.cf
relayhost = [smtp.smtpfa.st]:2525
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
# Postfix needs the SASL modules for PLAIN/LOGIN authentication
sudo apt install libsasl2-modules
# the password file, readable by root only
echo "[smtp.smtpfa.st]:2525 smtpfast:YOUR_SMTPFAST_API_KEY" | sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix
smtp_tls_security_level = encrypt makes Postfix refuse to send if TLS cannot be negotiated (use secure if you also want it to verify the certificate), and the square brackets around the host tell Postfix to connect to that host directly instead of looking up MX records.
Two more things for system mail. Mail for root usually stays on the machine, so point it at a real inbox in /etc/aliases (root: [email protected], then sudo newaliases). And the sender is typically root@your-droplet-hostname, which is not a domain you have verified, so SMTPfast will reject it. Rewrite it to an address on your verified domain:
# /etc/postfix/main.cf
smtp_generic_maps = hash:/etc/postfix/generic
# /etc/postfix/generic maps the local sender to a verified one
echo "root@your-droplet-hostname [email protected]" | sudo tee /etc/postfix/generic
sudo postmap /etc/postfix/generic
sudo systemctl reload postfix
Whichever you pick, set up your domain
Neither option helps if the receiving side does not trust your domain. Before you send real mail, verify your sending domain so SPF, DKIM and DMARC line up with the from address. In SMTPfast that is a few DNS records, and the domain authentication guide walks through each one. If your DNS is at DigitalOcean too, you add the records under Networking, Domains, in the DigitalOcean control panel.
Summary
| Your app | Use | Port |
|---|---|---|
| Your own code, any language | HTTPS API | 443 |
| Code that already uses the Resend SDK | The same SDK, SMTPfast base URL | 443 |
| Only speaks SMTP (WordPress, Ghost, frameworks, Postfix) | SMTP with STARTTLS | 2525 |
| Anything on 25, 465 or 587 | Nothing: blocked on Droplets by default | none |
DigitalOcean's block is not a bug to fight. It is the platform telling you the same thing their docs say outright: do not run your own mail server on a Droplet, use an email service. Pick the HTTPS API when you write the code, port 2525 when you cannot change it, and your password reset emails will arrive before the user gives up waiting.
Create a free SMTPfast account to get an API key, or read the API documentation first.
Ready to get started?
Start sending transactional email today. Free to start, no credit card required.
Get Started for FreeRelated Posts
One approval instead of five DNS records: Domain Connect on SMTPfast
If your domain is on Cloudflare, SMTPfast can publish its sending records, and separately the inbound MX, through Domain Connect. You review each set on Cloudflare's screen, approve, and the records are written for you. Here is how it works, what gets written, and what to do when your DNS lives elsewhere.
Logs API and webhook deliveries: answer 'did it send?' without opening the dashboard
Two additions for the debugging side of email: the event log behind the Logs page is now an API, and every webhook has a delivery log with attempts, responses, automatic retries and a retry button.