Sign inSign up

chrroessner/postfix

By chrroessner

•Updated about 10 hours ago

Postfix on Alpine with dynamic maps, env config, custom overlays, and TLSRPT.

Image
0

10K+

chrroessner/postfix repository overview

⁠Postfix on Alpine

Publish Docker Image License: MIT Last Commit Stars

This project builds a complete Postfix image on Alpine Linux, compiles a pinned Postfix version directly from source, enables SMTPUTF8/EAI, enables dynamic lookup tables and optional databases, and links Postfix against libtlsrpt so that TLSRPT works end-to-end.

The image uses a clean multi-stage build, pinned upstream sources, predictable runtime defaults, GitHub Actions for publishing, and a runtime model that is environment-driven first without blocking fully custom main.cf, master.cf, and map files when you need exact control.

⁠Table of Contents

⁠Goals

  • Alpine-based runtime
  • Pinned Postfix source version via Docker build args
  • Pinned libtlsrpt build and link integration
  • Full-featured lookup support, including common database and dynamic map modules
  • Runtime configuration via POSTFIX_* and POSTFIXMASTER_*
  • Support for _FILE secret variants for every environment variable
  • Optional full replacement of main.cf and master.cf
  • Support for custom maps, map compilation hooks, and init scripts
  • SMTPUTF8/EAI support through ICU
  • Clean container logging via stdout using postlogd
  • GitHub Actions for publishing and upstream checks

⁠Project Structure

.
├── Dockerfile
├── docker-entrypoint.sh
├── docker-healthcheck.sh
├── README.md
├── .env.example
├── .gitignore
├── Makefile
├── defaults
│   ├── main.cf
│   └── master.cf
├── examples
│   ├── docker-compose.yml
│   ├── custom-config
│   │   ├── main.cf.d
│   │   └── master.cf.d
│   ├── init
│   └── maps
└── .github
    └── workflows
        ├── docker-publish.yml
        └── postfix-upstream-check.yml

⁠Included Components

The image is built in two stages:

  • A builder stage compiles Postfix from the official source tarball, builds libtlsrpt, and builds tinycdb
  • A runtime stage keeps Alpine as the base image and only ships the required runtime libraries and the compiled Postfix payload

Current pinned defaults in this repository:

  • Postfix 3.11.7
  • libtlsrpt 0.5.0
  • Alpine 3.24

The running container exposes the usual built-in Postfix table types plus dynamic lookups such as:

  • cdb
  • ldap
  • lmdb
  • memcache
  • mongodb
  • mysql
  • nis
  • pcre
  • pgsql
  • sqlite
  • and the regular built-in lookup types such as hash, btree, cidr, regexp, socketmap, tcp, texthash, unionmap, unix

SDBM is not included on Alpine because the runtime does not provide the sdbm.h system interface required by Postfix's optional SDBM plugin.

The default master.cf also includes:

  • smtp
  • submission
  • submissions
  • postlog / postlogd for container-friendly logging

The Postfix build also includes SMTPUTF8/EAI support. The runtime default is smtputf8_enable = yes.

⁠Quick Start

⁠1. Build
docker build -t postfix .

To pin an explicit upstream release:

docker build \
  --build-arg POSTFIX_VERSION=3.11.7 \
  --build-arg POSTFIX_SHA256=a2f3242345753448072177fae83c322a403c9263696996406201145dab8e8625 \
  -t postfix:3.11.7 .

Multi-arch build with buildx:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --build-arg POSTFIX_VERSION=3.11.7 \
  --build-arg POSTFIX_SHA256=a2f3242345753448072177fae83c322a403c9263696996406201145dab8e8625 \
  -t postfix:3.11.7 \
  .
⁠2. Prepare environment
cp .env.example .env
⁠3. Start with Compose
make compose-up
⁠4. Inspect the running configuration
docker exec postfix postconf -n
docker exec postfix postconf -m
docker exec postfix postconf smtputf8_enable smtp_tlsrpt_enable smtp_tlsrpt_socket_name maillog_file

⁠Publishing

This repository includes a GitHub Actions workflow at .github/workflows/docker-publish.yml that publishes the maintainer image to Docker Hub as chrroessner/postfix.

It also includes .github/workflows/postfix-upstream-check.yml, which runs daily, checks the official Postfix release directory for a newer upstream tarball, refreshes the pinned SHA256, and opens or updates a pull request automatically when the pinned version in this repository is behind upstream.

The workflow runs:

  • on pushes to main
  • on pushes to master
  • on Git tags matching v*
  • daily via schedule
  • manually via workflow_dispatch

Required GitHub repository secrets:

  • DOCKERHUB_USERNAME
  • DOCKERHUB_TOKEN

Recommended Docker Hub setup:

  • create a public repository in your own namespace, for example <your-namespace>/postfix
  • create a Docker Hub access token dedicated to CI
  • keep latest for the default branch
  • publish release tags in the form v<postfix-version>-r<revision>, for example v3.11.7-r1

⁠License

The original container scripts and build tooling are licensed under the MIT License. See LICENSE⁠. Postfix source and the Postfix patch series are distributed under IPL-1.0, with existing per-file notices preserved; they are not covered by the repository's blanket MIT license. See NOTICE.md⁠ and Postfix license⁠.

This is an RNS-maintained customized Postfix image, not an official upstream Postfix image. Upstream submission or acceptance is not required to distribute our modifications and must not be inferred from their inclusion here.

Every image contains the exact patched Postfix sources, libtlsrpt and tinycdb sources, their original license files, the Dockerfile, patches and source metadata in /usr/share/doc/postfix-custom/sources/build-sources.tar.gz. Extract them without starting a mail server:

docker run --rm --entrypoint cat chrroessner/postfix:3.11.7-r1 \
  /usr/share/doc/postfix-custom/sources/build-sources.tar.gz > build-sources.tar.gz

Use an image digest instead of a mutable tag when retrieving sources for a specific deployment. SHA256SUMS is stored beside the archive. Sources remain available with that image even if upstream download locations change. Alpine packages remain under their individual licenses; the archive covers the three components compiled by this Dockerfile, not all Alpine package sources.

⁠Included Postfix patches

The table describes the current build. Patch filenames identify the qualified upstream version; image revisions distinguish our releases from upstream.

PatchCurrent imageUpstream statusPurpose
SASL EXTERNAL / client certificates⁠3.11.7-r1Downstream; unchanged from previous 3.11.7 buildPass verified certificate SAN identity and fingerprint to Dovecot SASL/PfxHTTP; optional full-chain CRLs with TLS resumption disabled.
Internal origin⁠3.11.7-r1Backport of Wietse Venema's final implementation in postfix-3.12-20260915Expose {postfix_internal_origin} as bounce, notify, verify, or absent/empty, using upstream provenance semantics.

The original two downstream DSN-origin patches have been removed. Older 3.11.7 images (before revision r1) exported {postfix_dsn_origin} with internal/external. That interface is no longer provided or aliased. Consumers must migrate together with this image. Pin the revision and digest; the floating 3.11.7 tag alone does not identify the macro contract.

Upstream Postfix 3.11.7 itself does not include this feature. The final implementation was published in the official 3.12 development snapshot on 2026-09-15. This image keeps the stable 3.11.7 base and backports only that feature, preserving its values, behavior and author attribution. See backport provenance⁠ for source identity, mechanical adaptations and migration checks.

The published container image additionally includes Postfix, which is distributed under IPL-1.0, plus bundled runtime dependencies such as libtlsrpt and tinycdb. Because of that, the OCI image metadata declares a combined license expression.

⁠Operating Model

On startup, the following happens:

  1. The entrypoint script prepares runtime directories under /var/spool/postfix and /var/lib/postfix.
  2. It installs the base main.cf, master.cf, and dynamicmaps.cf.
  3. If present, it overlays:
    • /etc/postfix/custom-config/main.cf
    • /etc/postfix/custom-config/master.cf
    • /etc/postfix/custom-config/dynamicmaps.cf
  4. It appends snippet directories in lexical order:
    • /etc/postfix/custom-config/main.cf.d/*.cf
    • /etc/postfix/custom-config/master.cf.d/*.cf
    • /etc/postfix/custom-config/dynamicmaps.cf.d/*.cf
  5. It applies runtime defaults and then processes environment variables:
    • POSTFIX_* for main.cf
    • POSTFIXMASTER_* for master.cf
  6. It runs .sh files from /docker-entrypoint-init.d
  7. It compiles standard maps and any maps declared in POSTFIX_RUNTIME_POSTMAPS
  8. It runs postfix check and finally starts postfix start-fg

⁠Important Volumes / Mountpoints

Path in ContainerPurpose
/etc/postfix/custom-configFull replacement files or config snippets
/etc/postfix/mapsCustom map files
/docker-entrypoint-init.dInit hooks (.sh)
/etc/postfix/certsTLS certificates and keys
/var/spool/postfixQueue data if you want persistence
/var/lib/postfixRuntime-owned Postfix data directory

⁠Environment Variables

⁠Runtime defaults
VariableDefaultMeaning
POSTFIX_RUNTIME_LOG_TO_STDOUTtrueEnable maillog_file = /dev/stdout
POSTFIX_RUNTIME_HOSTNAMEderivedSets myhostname
POSTFIX_RUNTIME_DOMAINderivedSets mydomain
POSTFIX_RUNTIME_DESTINATIONSderivedSets mydestination
POSTFIX_RUNTIME_MYNETWORKS127.0.0.0/8 [::1]/128Sets mynetworks
POSTFIX_RUNTIME_AUTO_POSTMAP_STANDARDtrueCompiles standard text maps
POSTFIX_RUNTIME_POSTMAPSemptyComma-separated extra maps, for example lmdb:/etc/postfix/maps/transport,lmdb:/etc/postfix/maps/routes
POSTFIX_RUNTIME_RUN_SCRIPTStrueRuns /docker-entrypoint-init.d/*.sh
POSTFIX_RUNTIME_TLSRPT_SOCKET_NAMErun/tlsrpt/tlsrpt.sockSets smtp_tlsrpt_socket_name
⁠Generic main.cf overrides

Every variable named POSTFIX_<parameter> becomes:

<parameter> = <value>

The suffix is used verbatim as the Postfix parameter name. That matters for parameters with embedded uppercase segments such as CAfile.

Examples:

  • POSTFIX_relayhost=[smtp.example.net]:587
  • POSTFIX_smtpd_tls_cert_file=/etc/postfix/certs/tls.crt
  • POSTFIX_smtpd_tls_CAfile=/etc/postfix/certs/ca.crt
  • POSTFIX_smtp_tlsrpt_enable=yes
  • POSTFIX_smtputf8_enable=yes
  • POSTFIX_transport_maps=lmdb:/etc/postfix/maps/transport

Every variable also supports a _FILE variant:

  • POSTFIX_relayhost_FILE=/run/secrets/postfix_relayhost
  • POSTFIX_sasl_passwd_FILE=/run/secrets/postfix_sasl_passwd
⁠Generic master.cf overrides

Every variable named POSTFIXMASTER_<selector> becomes a postconf -P update.

Encoding rules:

  • __ becomes /
  • ___ becomes -

The remaining characters are preserved verbatim, so use the exact Postfix parameter spelling after the service selector.

Examples:

  • POSTFIXMASTER_submission__inet__syslog_name=postfix/submission
  • POSTFIXMASTER_submission__inet__smtpd_tls_security_level=encrypt
  • POSTFIXMASTER_smtps___inet__smtpd_upstream_proxy_protocol=haproxy

⁠Custom Configuration and Maps

You have three supported customization levels.

⁠1. Env-driven

Use POSTFIX_* and POSTFIXMASTER_* for most installations. This is the intended fast path.

⁠2. File overlays

Mount custom files into /etc/postfix/custom-config:

  • main.cf
  • master.cf
  • dynamicmaps.cf

Or mount snippets into:

  • main.cf.d
  • master.cf.d
  • dynamicmaps.cf.d
⁠3. Custom maps

Mount map files under /etc/postfix/maps and either:

  • reference them directly from POSTFIX_*
  • or request compilation via POSTFIX_RUNTIME_POSTMAPS

Example:

docker run --rm \
  -e POSTFIX_transport_maps=lmdb:/etc/postfix/maps/transport \
  -e POSTFIX_RUNTIME_POSTMAPS=lmdb:/etc/postfix/maps/transport \
  -v $(pwd)/examples/maps:/etc/postfix/maps \
  postfix

Important for generated map types such as hash, cdb or lmdb:

  • postmap writes the compiled database next to the source file
  • the mounted map directory must therefore be writable, or you must mount precompiled map files instead

Important note when replacing master.cf completely:

  • if POSTFIX_RUNTIME_LOG_TO_STDOUT=true, your custom master.cf should keep the postlog / postlogd services
  • if you intentionally remove them, also set POSTFIX_RUNTIME_LOG_TO_STDOUT=false

⁠SMTPUTF8 / EAI

SMTPUTF8 support is compiled in through ICU and defaults to enabled:

  • smtputf8_enable = yes

You can still disable it for a specific deployment:

docker run --rm \
  -e POSTFIX_smtputf8_enable=no \
  postfix

SMTPUTF8 also has to be supported by the rest of the mail path, including content filters, LMTP servers, and downstream SMTP servers.

⁠TLS and TLSRPT

TLSRPT support is a hard requirement in this image:

  • libtlsrpt is built from source
  • Postfix is compiled with -DUSE_TLSRPT
  • Postfix links against -ltlsrpt

The container default is:

  • smtp_tlsrpt_enable = no
  • smtp_tlsrpt_socket_name = run/tlsrpt/tlsrpt.sock

That socket name is relative to the Postfix queue directory, so the effective default path inside the container is:

/var/spool/postfix/run/tlsrpt/tlsrpt.sock

Typical integration pattern:

  1. Run a TLSRPT collector sidecar or companion process that exposes a Unix socket.
  2. Share the socket path with the Postfix container.
  3. Set POSTFIX_smtp_tlsrpt_enable=yes.
  4. If needed, override POSTFIX_RUNTIME_TLSRPT_SOCKET_NAME.

⁠Local DSN origin for Milters

Since image revision 3.11.7-r1, the build applies Wietse Venema's final upstream internal-origin implementation from postfix-3.12-20260915, backported to the pinned stable source with a version guard and SHA-256 verification. There is no old-macro fallback.

{postfix_internal_origin}Upstream meaning
bounceLocally generated delivery notification, including double bounces and postmaster copies.
notifyLocally generated SMTP session transcript.
verifyAddress-verification probe; Milters must leave it unchanged.
absent or emptyOther messages, including SMTP/QMQP/sendmail submissions and internal alias/forward processing.

The macro is included in the default milter_connect_macros. Explicit operator macro lists must include it if CONNECT delivery is needed. A Milter can also request it through normal macro negotiation, including at end of headers.

This macro proves provenance, not permission to sign any message. A DKIM2 DSN adapter must retain its null-envelope-sender, recipient, report-structure and embedded-message validation. notify, verify, and non-null-sender postmaster/double-bounce messages must not be mistaken for normal DSNs.

Deploy the matching adapter and image in a coordinated change. The old adapter will not recognize the new macro and may pass bounces without signing them; the new adapter cannot use the removed downstream macro. Rollback must restore the old Postfix image, adapter and configuration together.

⁠SASL EXTERNAL with client certificates

The pinned Postfix 3.11.7 source is patched at build time with patches/postfix-3.11.7-sasl-external-client-cert.patch. The build verifies the patch checksum and refuses to apply this version-specific patch to another Postfix release.

The patch bridges a verified TLS client identity to the Dovecot authentication protocol used by Postfix. EXTERNAL is advertised and accepted for an SMTP session only when all of these conditions are true:

  • TLS is active and OpenSSL marks the client certificate chain as trusted
  • the leaf certificate has exactly one distinct rfc822Name SAN value
  • repeated identical rfc822Name values deduplicate to that one identity
  • the SAN is printable ASCII and contains no NUL, tab, CR, LF, or other protocol-control bytes
  • a certificate fingerprint is available

There is no Common Name fallback. A missing SAN, multiple different mail SANs, an unsafe SAN, an untrusted chain, or a missing fingerprint removes EXTERNAL from that session while leaving other configured SASL mechanisms available. If the backend offers only EXTERNAL and the current session has no eligible certificate identity, Postfix continues the SMTP session without announcing or accepting AUTH; this condition does not terminate the smtpd process. The same applies when the certificate-aware mechanism list still contains other mechanisms but smtpd_sasl_mechanism_filter removes all of them for the session, for example an external-only static filter after EXTERNAL was removed because no verified client identity is available.

For an eligible AUTH EXTERNAL request, Postfix adds exactly these fields to the tab-delimited Dovecot auth request:

ssl_client_verify=SUCCESS
ssl_client_san_email=<rfc822Name>
ssl_client_fingerprint=<hex[:hex...]>

The verify and fingerprint values are generated internally. The certificate identity is validated before it reaches this wire format. The receiving auth service must still reject duplicate security fields and must perform its normal identity lookup and authorization-ID policy.

Client certificates should remain optional when password or OAuth clients are also supported:

smtpd_tls_ask_ccert = yes
smtpd_tls_req_ccert = no

Configure smtpd_tls_CAfile with only the issuing CAs that are trusted for client authentication. Do not add personal client certificates to relay_clientcerts; SASL EXTERNAL must follow the regular authenticated-user and account-policy path.

⁠CRL enforcement

Upstream OpenSSL chain validation does not enable revocation checks merely because a certificate contains CRL distribution points. This patch therefore adds the opt-in Postfix parameters:

  • smtpd_tls_crl_file (default: empty)
  • tlsproxy_tls_crl_file (default: $smtpd_tls_crl_file)

When smtpd_tls_crl_file is non-empty, Postfix loads PEM CRLs from that file and enables both X509_V_FLAG_CRL_CHECK and X509_V_FLAG_CRL_CHECK_ALL. The file must contain current CRLs for every issuer in the verified chain. A revoked certificate and a missing or expired required CRL make the client certificate untrusted, so EXTERNAL is not offered. Because the client certificate remains optional, the TLS connection can still use another SASL mechanism.

Failure to load the explicitly configured CRL file disables TLS support rather than silently continuing without revocation checks. Leaving the parameter empty preserves upstream behavior and does not enable CRL checking. A combined CA-and-CRL PEM bundle may be used for both smtpd_tls_CAfile and smtpd_tls_crl_file. Reload Postfix after replacing the CRL bundle so that new SMTP server and TLS proxy processes load the updated revocation state.

While CRL checking is enabled, server-side TLS session caching and all session tickets are disabled, including TLS 1.3 tickets. Every new SMTP connection therefore performs a full certificate verification against the currently loaded CRLs; a session verified before a revocation cannot bypass that check through TLS resumption after a reload.

⁠Container Logging

This image follows the Postfix container logging model documented upstream:

  • postfix start-fg
  • maillog_file = /dev/stdout
  • postlogd wired in through master.cf

That gives you standard container log collection without a syslog daemon in the image.

If you prefer a different logging setup:

  • set POSTFIX_RUNTIME_LOG_TO_STDOUT=false
  • provide your own main.cf / master.cf

⁠Health Check

The image includes a simple health check based on:

postfix status

⁠Development / Convenience

Build locally:

make build

Run a local smoke check:

make test-smoke

Create a local SBOM export:

make sbom-local

Inspect registry SBOM data after push:

make sbom-registry IMAGE_NAME=<your-namespace>/postfix TAG=latest

⁠References

Tag summary

Content type

Image

Digest

sha256:2b3cbc331…

Size

24.2 MB

Last updated

about 10 hours ago

docker pull chrroessner/postfix