Postfix mail server image on Debian Trixie, configured entirely via POSTFIX_* variables
50K+
A flexible and modern Docker image for a Postfix mail server, based on Debian Trixie.
POSTFIX_* environment variables — no config files to edit. Per-service master.cf overrides via POSTFIXMASTER__*.HOLD_RELEASE_CHECK_URL).submission (587) and submissions (465) with TLS and SASL authentication out of the box; both accept mail from authenticated clients only.postfix-ldap and postfix-mysql for user lookups, virtual mailboxes and aliases.docker-entrypoint.d/ to extend the startup sequence.postfix check and postfix upgrade-configuration on every start.docker stop.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
On container start, the entrypoint executes all scripts inside /docker-entrypoint.d/
in lexicographic order via run-parts, then starts Postfix:
| Script | Purpose |
|---|---|
10-copy | Copies LDAP .cf files from /config/ldap/ into /etc/postfix/ldap/ |
20-setup-master | Defines 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-postconf | Translates all POSTFIX_* environment variables into main.cf settings |
26-postconf-master | Translates all POSTFIXMASTER__* environment variables into per-service master.cf overrides |
30-maintenance | Clears stale PID files, runs postfix check and postfix upgrade-configuration |
99-end | Signals the end of initialisation |
Custom scripts added via derived images are sorted into this sequence by their filename prefix.
ENTRYPOINT_QUIET_LOGSSuppresses 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 settingsEvery 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 overridesSets 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 releaseWhen 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).
POSTFIX_* variables| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_myhostname | myhostname | smtp.example.com |
POSTFIX_mydomain | mydomain | example.com |
POSTFIX_mydestination | mydestination | $myhostname |
POSTFIX_myorigin | myorigin | $mydomain |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_inet_interfaces | inet_interfaces | all |
POSTFIX_inet_protocols | inet_protocols | all |
POSTFIX_mynetworks | mynetworks | 127.0.0.0/8 10.0.0.0/8 |
Security note: Keep
mynetworksas narrow as possible. Hosts listed here can relay mail without authentication.
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_relayhost | relayhost | [smtp.relay.com]:587 |
POSTFIX_virtual_transport | virtual_transport | lmtp:dovecot:24 |
| Variable | Postfix parameter | Description |
|---|---|---|
POSTFIX_virtual_mailbox_domains | virtual_mailbox_domains | Domains hosted on this server |
POSTFIX_virtual_mailbox_maps | virtual_mailbox_maps | Map addresses to mailbox paths |
POSTFIX_virtual_alias_maps | virtual_alias_maps | Forwarding and alias map |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_smtpd_tls_cert_file | smtpd_tls_cert_file | /etc/postfix/certs/tls.crt |
POSTFIX_smtpd_tls_key_file | smtpd_tls_key_file | /etc/postfix/certs/tls.key |
POSTFIX_smtpd_tls_security_level | smtpd_tls_security_level | may |
POSTFIX_smtpd_tls_protocols | smtpd_tls_protocols | >=TLSv1.2 |
POSTFIX_smtpd_tls_mandatory_protocols | smtpd_tls_mandatory_protocols | >=TLSv1.2 |
POSTFIX_smtpd_tls_ciphers | smtpd_tls_ciphers | high |
POSTFIX_smtpd_tls_loglevel | smtpd_tls_loglevel | 1 |
POSTFIX_smtpd_tls_session_cache_database | smtpd_tls_session_cache_database | btree:/var/spool/postfix/smtpd_scache |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_smtp_tls_cert_file | smtp_tls_cert_file | /etc/postfix/certs/tls.crt |
POSTFIX_smtp_tls_key_file | smtp_tls_key_file | /etc/postfix/certs/tls.key |
POSTFIX_smtp_tls_security_level | smtp_tls_security_level | dane |
POSTFIX_smtp_tls_protocols | smtp_tls_protocols | >=TLSv1.2 |
POSTFIX_smtp_tls_loglevel | smtp_tls_loglevel | 1 |
POSTFIX_smtp_tls_policy_maps | smtp_tls_policy_maps | MTA-STS socketmap URI |
POSTFIX_smtp_dns_support_level | smtp_dns_support_level | dnssec (required for DANE) |
POSTFIX_smtp_tls_session_cache_database | smtp_tls_session_cache_database | btree:/var/spool/postfix/smtp_scache |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_smtpd_sasl_type | smtpd_sasl_type | dovecot |
POSTFIX_smtpd_sasl_path | smtpd_sasl_path | inet:dovecot:8745 |
POSTFIX_smtpd_sasl_auth_enable | smtpd_sasl_auth_enable | yes |
POSTFIX_smtpd_tls_auth_only | smtpd_tls_auth_only | yes |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_smtpd_milters | smtpd_milters | inet:rspamd:11332 |
POSTFIX_non_smtpd_milters | non_smtpd_milters | inet:rspamd:11332 |
POSTFIX_milter_default_action | milter_default_action | tempfail |
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.
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_smtpd_client_connection_rate_limit | smtpd_client_connection_rate_limit | 50 |
POSTFIX_smtpd_client_message_rate_limit | smtpd_client_message_rate_limit | 100 |
POSTFIX_smtpd_client_recipient_rate_limit | smtpd_client_recipient_rate_limit | 50 |
POSTFIX_anvil_rate_time_unit | anvil_rate_time_unit | 60s |
| Variable | Postfix parameter | Example value |
|---|---|---|
POSTFIX_message_size_limit | message_size_limit | 52428800 (50 MB) |
POSTFIX_maillog_file | maillog_file | /dev/stdout |
POSTFIX_masquerade_domains | masquerade_domains | example.com |
For the full list of Postfix parameters, see the Postfix documentation.
| Port | Protocol | Purpose |
|---|---|---|
25/tcp | SMTP | Server-to-server mail transfer (inbound MX) |
587/tcp | Submission | Authenticated client mail submission (STARTTLS + SASL) |
465/tcp | Submissions (SMTPS) | Submission with implicit TLS (RFC 8314), SASL required; Postfix service name submissions |
/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
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
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
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.
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.
The default CMD is postfix start-fg. You can override it for debugging:
docker run --rm -it ricariel/postfix bash
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:
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:
| Change | Impact |
|---|---|
permit_naked_ip_address, check_relay_domains, reject_maps_rbl removed | Postfix 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-only | The 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 default | Clients that pipeline SMTP commands without RFC 2920 authorization are immediately disconnected. Affects broken senders. |
| Bare newline / SMTP smuggling defenses enabled by default | Non-$mynetworks clients that do not send proper <CR><LF>.<CR><LF> will stall. |
MySQL charset changed from latin1 → utf8mb4 | Existing postfix-mysql lookup tables may need charset conversion. |
postconf now warns if a # comment appears on the same line as a parameter value — move
inline comments to their own line.This project is licensed under the MIT License. See the LICENSE file for details.
Content type
Image
Digest
sha256:b032cc581…
Size
96.7 MB
Last updated
about 11 hours ago
docker pull ricariel/postfix