Sign inSign up

ricariel/postfix

By ricariel

•Updated about 11 hours ago

Postfix mail server image on Debian Trixie, configured entirely via POSTFIX_* variables

Image
0

50K+

ricariel/postfix repository overview

⁠Docker Postfix

Docker Pulls Image Size License

A flexible and modern Docker image for a Postfix mail server, based on Debian Trixie.

⁠Features

  • Debian Trixie: Built on a modern base image with Postfix 3.10.x.
  • Flexible Configuration: Configure any Postfix parameter via POSTFIX_* environment variables — no config files to edit. Per-service master.cf overrides via POSTFIXMASTER__*.
  • Milter outages without lost mail: Optionally holds submitted mail while the Milter is down and releases it once the Milter is back (HOLD_RELEASE_CHECK_URL).
  • Secure by Default: Enables submission (587) and submissions (465) with TLS and SASL authentication out of the box; both accept mail from authenticated clients only.
  • Database Integration: Includes postfix-ldap and postfix-mysql for user lookups, virtual mailboxes and aliases.
  • Extensible: Drop additional scripts into docker-entrypoint.d/ to extend the startup sequence.
  • Automated Maintenance: Runs postfix check and postfix upgrade-configuration on every start.
  • Container-ready: Logs to stdout, handles signals for graceful shutdown and exits with status 0 on docker stop.

⁠Quickstart

docker run -d --name postfix \
  -e POSTFIX_myhostname=mail.example.com \
  -e POSTFIX_mydomain=example.com \
  -e POSTFIX_mynetworks="127.0.0.0/8" \
  -p 25:25 \
  -p 587:587 \
  -p 465:465 \
  ricariel/postfix

⁠Startup Sequence

On container start, the entrypoint executes all scripts inside /docker-entrypoint.d/ in lexicographic order via run-parts, then starts Postfix:

ScriptPurpose
10-copyCopies LDAP .cf files from /config/ldap/ into /etc/postfix/ldap/
20-setup-masterDefines submission (587) and submissions (465) in master.cf with postconf -M/-P: TLS, SASL, smtpd_relay_restrictions=permit_sasl_authenticated,reject, milter_macro_daemon_name=ORIGINATING
25-postconfTranslates all POSTFIX_* environment variables into main.cf settings
26-postconf-masterTranslates all POSTFIXMASTER__* environment variables into per-service master.cf overrides
30-maintenanceClears stale PID files, runs postfix check and postfix upgrade-configuration
99-endSignals the end of initialisation

Custom scripts added via derived images are sorted into this sequence by their filename prefix.

⁠Configuration

⁠Environment Variables
⁠ENTRYPOINT_QUIET_LOGS

Suppresses the startup log output of the entrypoint script itself. Postfix's own logs (via maillog_file) are unaffected.

-e ENTRYPOINT_QUIET_LOGS=1
⁠POSTFIX_* — Postfix main.cf settings

Every environment variable with the POSTFIX_ prefix is applied to /etc/postfix/main.cf at startup. The prefix is stripped; the remainder is used as the Postfix parameter name (case-sensitive).

Set a value:

-e POSTFIX_myhostname=mail.example.com
-e POSTFIX_mynetworks="127.0.0.0/8 10.0.0.0/8"

Delete / disable a setting (empty value comments it out in main.cf):

-e POSTFIX_relayhost=
⁠POSTFIXMASTER__* — per-service master.cf overrides

Sets a parameter for a single service only, as -o name=value in master.cf. The variable name holds service name, service type and parameter, separated by double underscores:

POSTFIXMASTER__<service>__<type>__<parameter>=<value>

26-postconf-master applies it with postconf -P "<service>/<type>/<parameter>=<value>". The service must exist in master.cf; otherwise the container stops with an error. An empty value removes the override.

-e POSTFIXMASTER__submission__inet__milter_default_action=quarantine
-e POSTFIXMASTER__submissions__inet__milter_default_action=quarantine
⁠HOLD_RELEASE_CHECK_URL, HOLD_RELEASE_INTERVAL — hold queue release

When HOLD_RELEASE_CHECK_URL is set, /usr/local/bin/hold-release runs next to Postfix. Every HOLD_RELEASE_INTERVAL seconds (default 60) it checks the hold queue. If it holds messages and the URL answers with HTTP 2xx, every held message is submitted again with sendmail (original envelope sender and recipients) and then deleted from the hold queue. The new copy passes non_smtpd_milters.

-e HOLD_RELEASE_CHECK_URL=http://rspamd:11334/ping
-e HOLD_RELEASE_INTERVAL=60

The helper releases every message in the hold queue. Do not combine it with other HOLD actions (header_checks, access maps, Milter quarantine).

⁠Reference: commonly used POSTFIX_* variables
⁠Core identity
VariablePostfix parameterExample value
POSTFIX_myhostnamemyhostnamesmtp.example.com
POSTFIX_mydomainmydomainexample.com
POSTFIX_mydestinationmydestination$myhostname
POSTFIX_myoriginmyorigin$mydomain
⁠Network
VariablePostfix parameterExample value
POSTFIX_inet_interfacesinet_interfacesall
POSTFIX_inet_protocolsinet_protocolsall
POSTFIX_mynetworksmynetworks127.0.0.0/8 10.0.0.0/8

Security note: Keep mynetworks as narrow as possible. Hosts listed here can relay mail without authentication.

⁠Relay
VariablePostfix parameterExample value
POSTFIX_relayhostrelayhost[smtp.relay.com]:587
POSTFIX_virtual_transportvirtual_transportlmtp:dovecot:24
⁠Virtual domains and mailboxes
VariablePostfix parameterDescription
POSTFIX_virtual_mailbox_domainsvirtual_mailbox_domainsDomains hosted on this server
POSTFIX_virtual_mailbox_mapsvirtual_mailbox_mapsMap addresses to mailbox paths
POSTFIX_virtual_alias_mapsvirtual_alias_mapsForwarding and alias map
⁠TLS — inbound (smtpd)
VariablePostfix parameterExample value
POSTFIX_smtpd_tls_cert_filesmtpd_tls_cert_file/etc/postfix/certs/tls.crt
POSTFIX_smtpd_tls_key_filesmtpd_tls_key_file/etc/postfix/certs/tls.key
POSTFIX_smtpd_tls_security_levelsmtpd_tls_security_levelmay
POSTFIX_smtpd_tls_protocolssmtpd_tls_protocols>=TLSv1.2
POSTFIX_smtpd_tls_mandatory_protocolssmtpd_tls_mandatory_protocols>=TLSv1.2
POSTFIX_smtpd_tls_cipherssmtpd_tls_ciphershigh
POSTFIX_smtpd_tls_loglevelsmtpd_tls_loglevel1
POSTFIX_smtpd_tls_session_cache_databasesmtpd_tls_session_cache_databasebtree:/var/spool/postfix/smtpd_scache
⁠TLS — outbound (smtp)
VariablePostfix parameterExample value
POSTFIX_smtp_tls_cert_filesmtp_tls_cert_file/etc/postfix/certs/tls.crt
POSTFIX_smtp_tls_key_filesmtp_tls_key_file/etc/postfix/certs/tls.key
POSTFIX_smtp_tls_security_levelsmtp_tls_security_leveldane
POSTFIX_smtp_tls_protocolssmtp_tls_protocols>=TLSv1.2
POSTFIX_smtp_tls_loglevelsmtp_tls_loglevel1
POSTFIX_smtp_tls_policy_mapssmtp_tls_policy_mapsMTA-STS socketmap URI
POSTFIX_smtp_dns_support_levelsmtp_dns_support_leveldnssec (required for DANE)
POSTFIX_smtp_tls_session_cache_databasesmtp_tls_session_cache_databasebtree:/var/spool/postfix/smtp_scache
⁠SASL authentication via Dovecot
VariablePostfix parameterExample value
POSTFIX_smtpd_sasl_typesmtpd_sasl_typedovecot
POSTFIX_smtpd_sasl_pathsmtpd_sasl_pathinet:dovecot:8745
POSTFIX_smtpd_sasl_auth_enablesmtpd_sasl_auth_enableyes
POSTFIX_smtpd_tls_auth_onlysmtpd_tls_auth_onlyyes
⁠Milter integration (Rspamd, ClamAV-milter, etc.)
VariablePostfix parameterExample value
POSTFIX_smtpd_milterssmtpd_miltersinet:rspamd:11332
POSTFIX_non_smtpd_miltersnon_smtpd_miltersinet:rspamd:11332
POSTFIX_milter_default_actionmilter_default_actiontempfail

Keeping mail while the Milter is down. With accept, Postfix lets mail through unfiltered while the Milter is unreachable, and outgoing mail leaves without the DKIM signature that e.g. Rspamd adds. With tempfail, remote mail servers retry later, but applications that send directly via SMTP often drop the message after the 4xx reply. A combination covers both:

# Port 25: remote servers retry on 4xx
-e POSTFIX_milter_default_action=tempfail
# Submission: accept and hold the mail, release it once the Milter is back
-e POSTFIXMASTER__submission__inet__milter_default_action=quarantine
-e POSTFIXMASTER__submissions__inet__milter_default_action=quarantine
-e HOLD_RELEASE_CHECK_URL=http://rspamd:11334/ping

Released mail reaches the Milter as local mail from localhost [127.0.0.1]. A requeue with postsuper -r would keep the original client address but lose the SASL login, so Rspamd would not sign mail from remote clients; that is why the helper uses sendmail. If the Milter is gone again during the release, the copy stays in the maildrop queue and pickup retries it. If the Milter rejects a released message (e.g. outgoing spam from a compromised account), it is not delivered; Postfix returns it to the sender as a non-delivery notification with the Milter's reason, where a direct submission would have got a 5xx reply.

The hold queue must survive a container restart, so /var/spool/postfix belongs on a persistent volume.

⁠Rate limiting
VariablePostfix parameterExample value
POSTFIX_smtpd_client_connection_rate_limitsmtpd_client_connection_rate_limit50
POSTFIX_smtpd_client_message_rate_limitsmtpd_client_message_rate_limit100
POSTFIX_smtpd_client_recipient_rate_limitsmtpd_client_recipient_rate_limit50
POSTFIX_anvil_rate_time_unitanvil_rate_time_unit60s
⁠Miscellaneous
VariablePostfix parameterExample value
POSTFIX_message_size_limitmessage_size_limit52428800 (50 MB)
POSTFIX_maillog_filemaillog_file/dev/stdout
POSTFIX_masquerade_domainsmasquerade_domainsexample.com

For the full list of Postfix parameters, see the Postfix documentation⁠.

⁠Ports
PortProtocolPurpose
25/tcpSMTPServer-to-server mail transfer (inbound MX)
587/tcpSubmissionAuthenticated client mail submission (STARTTLS + SASL)
465/tcpSubmissions (SMTPS)Submission with implicit TLS (RFC 8314), SASL required; Postfix service name submissions
⁠Volumes and Mounts
⁠/config/ldap/

Mount a directory containing Postfix LDAP lookup table configuration files (.cf files). They are copied into /etc/postfix/ldap/ at startup by 10-copy.

docker run -d --name postfix \
  -v /path/to/ldap-configs:/config/ldap:ro \
  ...
  ricariel/postfix

Example main.cf references:

virtual_mailbox_maps  = ldap:/etc/postfix/ldap/virtual_mailbox.cf
virtual_alias_maps    = ldap:/etc/postfix/ldap/aliases.cf
smtpd_sender_login_maps = ldap:/etc/postfix/ldap/sender_login.cf

A minimal LDAP lookup file looks like this:

server_host     = ldaps://ldap.example.com:636
bind            = yes
bind_dn         = cn=postfix,ou=services,dc=example,dc=com
bind_pw         = secret
search_base     = ou=users,dc=example,dc=com
query_filter    = (mail=%s)
result_attribute = mail
⁠TLS certificates

Mount your certificate and key files and point Postfix to them via environment variables:

docker run -d --name postfix \
  -v /etc/letsencrypt/live/smtp.example.com:/etc/postfix/certs:ro \
  -e POSTFIX_smtpd_tls_cert_file=/etc/postfix/certs/fullchain.pem \
  -e POSTFIX_smtpd_tls_key_file=/etc/postfix/certs/privkey.pem \
  ...
  ricariel/postfix
⁠Mail spool

Postfix stores its mail queue under /var/spool/postfix/. For persistence across container restarts (e.g. to avoid losing queued messages on restart), mount a volume:

-v postfix-spool:/var/spool/postfix
⁠SQL lookup tables

In addition to LDAP, postfix-mysql is included for MySQL/MariaDB-based setups. See example_sql_map.md⁠ for example database schemas and query configurations covering virtual mailboxes, aliases, forwarding, and catch-all rules.

⁠Extending the Image

⁠Adding custom startup scripts

Any executable file placed in /docker-entrypoint.d/ of a derived image is automatically executed at startup. Use the numeric prefix to control execution order relative to the built-in scripts (10, 20, 25, 26, 30, 99).

FROM ricariel/postfix

# Runs between 25-postconf and 30-maintenance
COPY --chmod=755 28-my-custom-setup.sh /docker-entrypoint.d/

Scripts must be executable — run-parts skips anything that is not. COPY --chmod=755 is the simplest way to guarantee that regardless of the permissions in the build context.

run-parts also ignores filenames containing a dot, so a script named 28-my-custom-setup.sh is not executed. Drop the extension: 28-my-custom-setup. That is why none of the bundled scripts have one.

⁠Overriding the default command

The default CMD is postfix start-fg. You can override it for debugging:

docker run --rm -it ricariel/postfix bash

⁠Docker Compose Example

services:
  postfix:
    image: ricariel/postfix
    restart: unless-stopped
    ports:
      - "25:25"
      - "587:587"
      - "465:465"
    volumes:
      - ./ldap:/config/ldap:ro
      - ./certs:/etc/postfix/certs:ro
      - postfix-spool:/var/spool/postfix
    environment:
      POSTFIX_myhostname: smtp.example.com
      POSTFIX_mydomain: example.com
      POSTFIX_mynetworks: "127.0.0.0/8"
      POSTFIX_virtual_mailbox_domains: "example.com"
      POSTFIX_virtual_transport: "lmtp:dovecot:24"
      POSTFIX_smtpd_tls_cert_file: /etc/postfix/certs/tls.crt
      POSTFIX_smtpd_tls_key_file: /etc/postfix/certs/tls.key
      POSTFIX_smtpd_tls_security_level: may
      POSTFIX_smtpd_sasl_type: dovecot
      POSTFIX_smtpd_sasl_path: "inet:dovecot:8745"
      POSTFIX_smtpd_sasl_auth_enable: "yes"
      POSTFIX_smtpd_milters: "inet:rspamd:11332"
      POSTFIX_non_smtpd_milters: "inet:rspamd:11332"
      POSTFIX_maillog_file: /dev/stdout

volumes:
  postfix-spool:

⁠Postfix Version Notes (3.8 → 3.10)

This image ships Postfix 3.10.x (Debian Trixie). If you are migrating from a Bookworm-based setup (Postfix 3.7.x), be aware of the following behavioral changes introduced across releases 3.8-3.10:

⁠Breaking changes (3.9)
ChangeImpact
permit_naked_ip_address, check_relay_domains, reject_maps_rbl removedPostfix refuses to start if any of these appear in smtpd_*_restrictions. Remove them from your config.
/usr/lib/postfix/configure-instance.sh is now internal-onlyThe script exits with code 1 when called directly. Postfix invokes it automatically as part of start-fg. Do not call it from entrypoint scripts.
smtpd_forbid_unauth_pipelining = yes now defaultClients that pipeline SMTP commands without RFC 2920 authorization are immediately disconnected. Affects broken senders.
Bare newline / SMTP smuggling defenses enabled by defaultNon-$mynetworks clients that do not send proper <CR><LF>.<CR><LF> will stall.
MySQL charset changed from latin1 → utf8mb4Existing postfix-mysql lookup tables may need charset conversion.
⁠TLS hardening (3.8)
  • "Export" and "low" cipher grades are silently promoted to "medium". Weak ciphers (3DES, RC4, SEED, IDEA, RC2, RC5) are no longer negotiated.
  • TLS renegotiation is unconditionally disabled.
  • postconf now warns if a # comment appears on the same line as a parameter value — move inline comments to their own line.
⁠Improvements (3.10)
  • OpenSSL 3.5 post-quantum cryptography support for outbound TLS.
  • Queue IDs are included in abnormal connection closure logs (better container log parsing).
  • SASL failure logs now include the mechanism name.
  • MySQL/PostgreSQL map connections reconnect immediately after a single failure (previously all queries were blocked for 60 seconds).

⁠License

This project is licensed under the MIT License. See the LICENSE⁠ file for details.

Tag summary

Content type

Image

Digest

sha256:b032cc581…

Size

96.7 MB

Last updated

about 11 hours ago

docker pull ricariel/postfix