Guide

Send Email From a Docker Container

Your Docker container can't reach SMTP port 25 on AWS, GCP, or Azure, so mail libraries hang. Send email over an HTTPS API on port 443 instead with one command.

Written by Aaron de Mello Senior Engineering Manager

VerifiedCLI 3.1.22 · Gmail · last tested June 19, 2026

Why can't a Docker container reach SMTP ports?

A container inherits its host's outbound network rules, and major clouds drop traffic to TCP port 25 at the platform edge. Google Cloud, AWS, and Azure all block port 25 by default so a compromised instance cannot turn into a spam relay. Port 25 is the default for server-to-server relay and for many mail libraries, which is why a container that "just sends email" stalls.

This is a documented platform policy, not a bug. The Google Cloud guide on sending email from instances states it plainly: "connections to destination TCP Port 25 are blocked when the destination is external to your VPC network." Google does permit the submission ports — it "does not place any restrictions" on outbound ports 587 or 465 — but those need authenticated submission per RFC 6409, and AWS and Azure restrict port 25 the same way. An HTTPS API skips the port question entirely.

What happens when a container tries SMTP?

The TCP handshake never completes. Your mail library opens a socket to port 25, the cloud silently drops the SYN packet, and the client waits for a reply that never arrives. Most SMTP libraries default to a connect timeout between 30 and 120 seconds before raising an error.

That hang is the tell. A blocked port produces a timeout, not a refusal, because a dropped packet looks identical to an unreachable host. The Amazon SES SMTP troubleshooting docs describe the same symptom and the same root cause, advising that "your ISP might be blocking port 25." A cloud VM blocks port 25 the same way. Port 587 may be reachable, but it still needs SMTP AUTH and TLS, so the simplest exit is HTTPS with no SMTP port at all. The code below reproduces the failure so you can recognize it in your own logs.

# Inside a container on a cloud VM this hangs ~30s, then fails
$ python3 -c "import smtplib; smtplib.SMTP('smtp.gmail.com', 25, timeout=30)"
TimeoutError: [Errno 110] Connection timed out

# A port check confirms the block on 25
$ nc -zv -w 5 smtp.gmail.com 25
nc: connect to smtp.gmail.com port 25 (tcp) timed out: Operation now in progress

Why does an HTTPS API on port 443 work?

Port 443 carries HTTPS, the same traffic every web request uses, so no cloud blocks it. An email API accepts a signed HTTPS request, queues the message on its own infrastructure, and relays it from IP addresses with an established sending reputation. Your container never opens an SMTP socket at all.

This sidesteps every problem in the previous two sections at once. There is no port 25 block to route around, no 30-second timeout, no SMTP AUTH handshake, and no local mail daemon to install. The Google Cloud guide reaches the same conclusion, recommending that instances use a third-party email service provider and send through its API rather than raw SMTP. One outbound HTTPS POST replaces the whole submission stack. The request below is the shape every email API follows: a JSON body over TLS to a single endpoint.

POST https://api.us.nylas.com/v3/grants/<grant_id>/messages/send
Content-Type: application/json
Authorization: Bearer <NYLAS_API_KEY>

{
  "to": [{ "email": "ops@example.com" }],
  "subject": "Deploy finished",
  "body": "Container build 482 shipped to production."
}

How do you authenticate the CLI inside a container?

A container has no browser and no system keyring, so the interactive login flow won't run. Pass three environment variables instead: NYLAS_API_KEY for the credential, NYLAS_GRANT_ID for the mailbox to send from, and NYLAS_DISABLE_KEYRING=1 so the tool stores nothing on disk.

Both NYLAS_API_KEY and NYLAS_GRANT_ID are required in headless mode; omit the grant ID and the send fails with a missing-grant error. Inject these at runtime through your orchestrator's secret store, never bake them into the image layer. Verify the credentials resolve with nylas email list, which makes a real authenticated call and returns within a second or two. Don't use nylas auth whoami for this check; listing messages exercises the same grant the send will use.

# Pass secrets at runtime, not in the image
export NYLAS_API_KEY="nyk_..."
export NYLAS_GRANT_ID="<grant_id>"
export NYLAS_DISABLE_KEYRING=1

# Confirm the credentials work before sending
nylas email list --limit 1

How do you send an email with the CLI?

The nylas email send command builds the JSON body, signs the request, and POSTs it over HTTPS on port 443. It takes --to, --subject, and --body, plus --yes to skip the confirmation prompt — required in a container, which has no TTY to answer it. It returns the message ID on success, one HTTPS call that completes in well under a second on a warm container.

This is the CLI analog of the raw API request shown earlier. Because it rides the same port 443 path, it works from a container where SMTP times out. Add --format json to get a machine-readable result you can pipe to jq, which is handy when a deploy script needs to log or assert on the returned message ID.

# Send over HTTPS (port 443) from inside the container.
# --yes skips the confirmation prompt, which is required without a TTY.
nylas email send \
  --to ops@example.com \
  --subject "Deploy finished" \
  --body "Container build 482 shipped to production." \
  --yes

# Capture the message ID for a deploy log
nylas email send \
  --to ops@example.com \
  --subject "Nightly backup OK" \
  --body "Backup completed at $(date -u)" \
  --yes \
  --format json \
  | jq -r '.id'

What does a minimal Dockerfile look like?

A working image needs three things: a base with curl and bash, the CLI installed from the official shell script, and the binary on PATH. The installer auto-detects architecture, pulls the latest release from GitHub, and verifies a SHA-256 checksum before writing the binary.

The install script lands the binary in ~/.config/nylas/bin, so add that directory to PATH in the image. Keep the secrets out of the Dockerfile entirely; the three env vars get injected at docker run time. This image is small enough to build in under a minute and adds no mail daemon, no Postfix, and no exposed SMTP port.

FROM debian:stable-slim

RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates bash \
  && rm -rf /var/lib/apt/lists/*

# Install the Nylas CLI from the official script (verifies SHA-256)
RUN curl -fsSL https://cli.nylas.com/install.sh | bash

ENV PATH="/root/.config/nylas/bin:${PATH}"
ENV NYLAS_DISABLE_KEYRING=1

# NYLAS_API_KEY and NYLAS_GRANT_ID are injected at runtime, not baked in
ENTRYPOINT ["nylas", "email", "send", "--yes"]
# Build, then inject secrets at run time
docker build -t mailer .
docker run --rm \
  -e NYLAS_API_KEY="nyk_..." \
  -e NYLAS_GRANT_ID="<grant_id>" \
  mailer --to ops@example.com --subject "Hi" --body "From a container"

What about attachments?

The nylas email send command does not expose a file-attachment flag today, so the CLI path covers text and HTML bodies but not arbitrary file payloads. For a deploy notification or an alert email, that limit rarely matters; the body holds the message and a link to logs or artifacts.

When you genuinely need to attach a file from a container, call the messages-send API endpoint directly with a multipart request, which accepts an attachment up to the API's documented size cap. The pattern is the same HTTPS-on-443 path the CLI uses, so it clears the SMTP block too. Keep the attachment small; multipart payloads over a few megabytes are better delivered as a signed download link in the body than inline.

# Attachments need the multipart API, not the send flag
curl -X POST "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/messages/send" \
  -H "Authorization: Bearer $NYLAS_API_KEY" \
  -F 'message={"to":[{"email":"ops@example.com"}],"subject":"Report","body":"See attached"};type=application/json' \
  -F 'file=@/tmp/report.pdf'

When should you call the API directly instead?

Reach for the raw HTTPS API when you need attachments or a payload the CLI flags don't cover, or when your image already ships a language runtime and an HTTP client. Use the CLI for plain text or HTML sends, deploy notifications, cron alerts, and any container where adding one small binary beats writing and maintaining an HTTP integration.

The trade-off is plumbing, not capability. A direct integration owns request signing, JSON construction, retry logic, and error parsing. The CLI owns all of that behind a single command, so a one-line send stays a one-line send. Both take the same port 443 exit, so whichever you pick, the SMTP timeout that started this guide never comes back.

Next steps