Postfix PostgreSQL Relay Image
A Postfix SMTP relay/MTA whose relay and transport decisions are driven by PostgreSQL lookup maps instead of static config. It receives inbound mail and forwards it to an internal SMTP target after checking the recipient's domain against a database — it has no knowledge of, and makes no assumptions about, what consumes the mail downstream.
This mirrors the Postfix setup described in the SimpleLogin self-hosting
documentation (postfix + postfix-pgsql, a relay_domains PostgreSQL map,
and a transport_maps PostgreSQL map): https://github.com/simple-login/app
No domain, hostname, or database credential is hardcoded in the image.
Everything is templated from environment variables by
docker-entrypoint.sh at container startup, then Postfix is started in the
foreground (postfix start-fg) so it runs correctly under Docker's process
supervision.
How it works
On startup, the entrypoint renders three files from templates under
/etc/postfix/templates/:
/etc/postfix/main.cf— core Postfix configuration./etc/postfix/pgsql-relay-domains.cf— PostgreSQL lookup that tells Postfix which domains it should relay mail for./etc/postfix/pgsql-transport-maps.cf— PostgreSQL lookup that tells Postfix where to forward accepted mail (the internal SMTP relay target).
Chroot is disabled for all Postfix services via postconf -F '*/*/chroot = n'
on every startup. Postfix's chrooted child processes (smtpd, trivial-rewrite,
etc.) lack access to /etc/resolv.conf inside the chroot jail, so they cannot
resolve Docker-internal hostnames — causing every PostgreSQL lookup to fail with
451 4.3.0 Temporary lookup failure. Disabling chroot is the standard remedy
in containerised deployments.
Environment variables
Required
| Variable | Description | Example |
|---|---|---|
MAIL_DOMAIN |
Mail domain this relay handles (mydomain/myorigin in main.cf). |
example.com |
MYHOSTNAME |
Hostname of this relay, used as Postfix's myhostname and as the MX target for MAIL_DOMAIN. |
mx1.example.com |
POSTGRES_HOST |
Hostname of the PostgreSQL server holding the lookup tables. | postgres |
POSTGRES_PORT |
PostgreSQL port. | 5432 |
POSTGRES_USER |
PostgreSQL user used for the lookup queries. | postfix |
POSTGRES_PASSWORD |
PostgreSQL password for POSTGRES_USER. |
change-me |
POSTGRES_DB |
PostgreSQL database name. | relaydb |
SMTP_RELAY_HOST |
Internal SMTP host that accepted mail is forwarded to after the relay-domains lookup matches. | app-internal |
SMTP_RELAY_PORT |
Port of the internal SMTP relay target. | 2525 |
Optional
| Variable | Description |
|---|---|
TLS_CERT_FILE |
Absolute path (inside the container) to a TLS certificate to use for smtpd_tls_cert_file. If unset, a self-signed certificate is generated on first start. |
TLS_KEY_FILE |
Absolute path (inside the container) to the private key matching TLS_CERT_FILE. Must be set together with TLS_CERT_FILE. |
The PostgreSQL lookup tables are expected to expose at least a
relay_domain(domain) table; both lookups query it by recipient domain.
TLS certificates
By default, if TLS_CERT_FILE/TLS_KEY_FILE are not provided, the
entrypoint generates a self-signed certificate at
/etc/postfix/ssl/snakeoil.{pem,key} on first start (matching the
self-signed fallback used in the upstream docs) and reuses it on subsequent
restarts if that path is kept on a persistent volume.
To use your own certificate instead, mount it into the container and point
TLS_CERT_FILE/TLS_KEY_FILE at the mounted paths, e.g.:
docker run \
-v /path/to/fullchain.pem:/certs/fullchain.pem:ro \
-v /path/to/privkey.pem:/certs/privkey.pem:ro \
-e TLS_CERT_FILE=/certs/fullchain.pem \
-e TLS_KEY_FILE=/certs/privkey.pem \
...
Usage
docker run
docker run -d \
-p 25:25 \
-e MAIL_DOMAIN=example.com \
-e MYHOSTNAME=mx1.example.com \
-e POSTGRES_HOST=postgres \
-e POSTGRES_PORT=5432 \
-e POSTGRES_USER=postfix \
-e POSTGRES_PASSWORD=change-me \
-e POSTGRES_DB=relaydb \
-e SMTP_RELAY_HOST=app-internal \
-e SMTP_RELAY_PORT=2525 \
gitea.arnodo.fr/damien/postfix-pgsql:latest
Docker Compose
services:
postfix:
image: gitea.arnodo.fr/damien/postfix-pgsql:latest
ports:
- "25:25"
environment:
MAIL_DOMAIN: example.com
MYHOSTNAME: mx1.example.com
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_USER: postfix
POSTGRES_PASSWORD: change-me
POSTGRES_DB: relaydb
SMTP_RELAY_HOST: app-internal
SMTP_RELAY_PORT: "2525"
Version management
The Debian base image tag is set in POSTFIX_VERSION (e.g. 12-slim),
which also pins the postfix/postfix-pgsql package versions available via
apt for that Debian release. To change it:
- Edit
POSTFIX_VERSIONwith the desired Debian tag. - Commit and push.
- The CI builds the image with that base and tags it with both
latestand the value ofPOSTFIX_VERSION.
Reference
- Upstream Postfix + PostgreSQL setup this image is based on: https://github.com/simple-login/app