Files
Ketan Patel 7ead00647b
Build and Push Docker Image / build (push) Successful in 1m25s
Publish Alpine musl distroless Postfix as the sole latest image
2026-10-03 00:17:09 -04:00

133 lines
5.4 KiB
Markdown

# 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
```sh
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:
```yaml
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
```sh
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:
```sh
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](LICENSE).
Issues: https://git.k2patel.in/k2patel/docker-postfix/issues