Docker Postfix SMTP Relay

Source: https://git.k2patel.in/k2patel/docker-postfix

k2patel/postfix:latest is the single maintained image: a distroless SMTP relay built from Alpine 3.24/musl into a scratch runtime. It supports Maileroo, Mailtrap, SendGrid, Gmail, Office 365, and generic upstream SMTP servers. There is no shell, BusyBox, package manager, Supervisor, or compiler in the image.

A native launcher configures Postfix, creates queue directories on fresh volumes, builds the SASL credential map, and starts Postfix in the foreground. Logs go to container stdout. Native Postfix tools remain available for queue administration.

Quick start

git clone https://git.k2patel.in/k2patel/docker-postfix.git
cd docker-postfix
cp env.sample .env
# Edit .env with your provider credentials and domain.
docker compose pull
docker compose up -d

To build locally, run docker compose build first. Builds include isolated SMTP relay tests; no test messages are sent to external mailboxes.

Configure applications to connect to postfix-relay:25 on the same Docker network. Only trusted networks should be allowed to relay.

Configuration

Variable Default Purpose
SMTP_SERVER Required Upstream SMTP hostname
SMTP_PORT 587 Upstream SMTP port
SMTP_USERNAME Required Upstream SMTP username
SMTP_PASSWORD Required Upstream SMTP password or token
DOMAIN Hostname suffix, otherwise localdomain Domain for outgoing mail; set explicitly
SERVER_HOSTNAME Container hostname Relay hostname; set to a valid FQDN
TIMEZONE America/New_York IANA timezone
LOCAL_NETWORK Detected IPv4 interface subnet Trusted local CIDR
SMTP_NETWORKS Empty Additional trusted IPv4 CIDRs, comma-separated
SMTP_HEADER_TAG Empty Optional RelayTag header value
SMTP_LISTEN_PORT 25 Host port in Compose
DATA_FOLDER Current directory Compose bind-mount base for logs/mail/spool
DEBUG no Enable Postfix debug level 2

Localhost and the local subnet are trusted. Invalid CIDRs and multiline configuration values are rejected. Credentials are stored in root-only files; the launcher does not print their contents.

Maileroo, Mailtrap, and SendGrid require STARTTLS. Generic providers retain opportunistic STARTTLS (smtp_tls_security_level=may). Use the provider's STARTTLS submission port, typically 587; implicit TLS on port 465 is not configured.

Provider examples are included in env.sample. For SendGrid, use the literal username apikey. Gmail requires an app password. Keep credentials in .env, which is excluded from Git and the Docker build context.

Updating an existing deployment

Use k2patel/postfix:latest, pull, and recreate the container. Existing environment settings and /var/spool/postfix persistence are retained. The default Compose file supplies the native health check. Replace an old custom postfix status or shell-based health check with:

healthcheck:
  test: ["CMD", "/usr/local/bin/postfix-entrypoint", "--healthcheck"]

The check connects to localhost SMTP and requires a 220 greeting. The old postfix status, postfix check, shell access, and mail/mailx commands are not part of this image. Use native commands below. Custom pipe/alias delivery commands that need a shell or external utilities are unsupported.

Operations

docker logs -f postfix-relay
docker exec postfix-relay /usr/local/bin/postfix-entrypoint --healthcheck
docker exec postfix-relay /usr/local/bin/postfix-entrypoint --check
docker exec postfix-relay postconf -n
docker exec postfix-relay postqueue -p
docker exec postfix-relay postqueue -f
docker compose restart postfix

--check parses and displays the active main and master configuration; startup also checks queue structure using postsuper. It does not validate upstream credentials or external delivery.

Host-side helpers work without a shell inside the container:

cd test
make help
make status
make queue
make check
# Sends a real email only when you explicitly run this command:
make test TO=recipient@example.com
# Optional sender, subject, or container:
make test TO=recipient@example.com FROM=sender@example.com SUBJECT='Relay test' CONTAINER=postfix-relay

Submission to the queue is not proof of delivery. Check logs for status=sent and inspect deferred messages with postqueue -p.

Build and CI

Gitea Actions builds linux/amd64 on the Kubernetes runner for pushes to main, monthly on the first day, and manual dispatch. Configure repository secrets DOCKER_USER and DOCKER_TOKEN for Docker Hub publishing. Each build uses a temporary Buildx builder and removes its cache afterward.

The final filesystem contains Postfix and its musl/shared-library dependencies, SASL and database plugins, ICU data, CA certificates, timezone data, and the native launcher. Go, Python, and packaging tools are confined to the build stage.

Before publication, a separate stage tests the exact shell-free filesystem: startup with an empty queue and stale PID file, native health checks, SMTP submission, STARTTLS and authenticated relay to a local mock server, header insertion, LMDB/PCRE plugins, queue draining, restart, and SIGTERM shutdown. The final image is copied from the pristine runtime, excluding test credentials and queue contents.

License and support

MIT License; see LICENSE. Issues: https://git.k2patel.in/k2patel/docker-postfix/issues

S
Description
Alpine/musl Postfix SMTP relay with support for multiple email providers
https://hub.docker.com/r/k2patel/postfix
Readme MIT
77 KiB
0 Stars 1 Watchers 0 Forks
Languages
Go 54.7%
Makefile 18.9%
Python 11.3%
Dockerfile 8.7%
Shell 6.4%