From 8143fff65f18d4fa22f941f14c61a7cee61470b3 Mon Sep 17 00:00:00 2001 From: Damien Date: Thu, 25 Jun 2026 11:12:46 +0200 Subject: [PATCH] feat(postfix-pgsql): add Dockerfile with templated config and entrypoint --- images/postfix-pgsql/Dockerfile | 38 ++++++++++++ images/postfix-pgsql/docker-entrypoint.sh | 62 +++++++++++++++++++ .../postfix-pgsql/templates/main.cf.template | 39 ++++++++++++ .../templates/pgsql-relay-domains.cf.template | 12 ++++ .../pgsql-transport-maps.cf.template | 15 +++++ 5 files changed, 166 insertions(+) create mode 100644 images/postfix-pgsql/Dockerfile create mode 100644 images/postfix-pgsql/docker-entrypoint.sh create mode 100644 images/postfix-pgsql/templates/main.cf.template create mode 100644 images/postfix-pgsql/templates/pgsql-relay-domains.cf.template create mode 100644 images/postfix-pgsql/templates/pgsql-transport-maps.cf.template diff --git a/images/postfix-pgsql/Dockerfile b/images/postfix-pgsql/Dockerfile new file mode 100644 index 0000000..2d520e5 --- /dev/null +++ b/images/postfix-pgsql/Dockerfile @@ -0,0 +1,38 @@ +# ============================================================================= +# Postfix + PostgreSQL Relay/MTA Image +# +# A pure SMTP relay/MTA: receives inbound mail and forwards it based on +# PostgreSQL-backed relay_domains/transport_maps lookups, equivalent to the +# Postfix setup described in the SimpleLogin self-hosting documentation +# (https://github.com/simple-login/app). No domain, hostname, or database +# credentials are baked into the image; everything is templated from +# environment variables at container startup. This image has no knowledge +# of what consumes the relayed mail. +# ============================================================================= + +# POSTFIX_VERSION pins the Debian base image tag (e.g. "12-slim"), which in +# turn pins the postfix/postfix-pgsql package versions available via apt. +ARG POSTFIX_VERSION=12-slim +FROM debian:${POSTFIX_VERSION} + +LABEL maintainer="Damien Arnodo" +LABEL description="Postfix SMTP relay/MTA with PostgreSQL-backed relay_domains and transport_maps lookups" + +RUN echo "postfix postfix/main_mailer_type select No configuration" | debconf-set-selections \ + && echo "postfix postfix/mailname string localhost" | debconf-set-selections \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + postfix \ + postfix-pgsql \ + gettext-base \ + openssl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY templates/ /etc/postfix/templates/ +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 25 + +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/images/postfix-pgsql/docker-entrypoint.sh b/images/postfix-pgsql/docker-entrypoint.sh new file mode 100644 index 0000000..9ab2416 --- /dev/null +++ b/images/postfix-pgsql/docker-entrypoint.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# ============================================================================= +# Renders Postfix config templates from environment variables and starts +# Postfix in the foreground. +# ============================================================================= +set -euo pipefail + +TEMPLATE_DIR="/etc/postfix/templates" +SSL_DIR="/etc/postfix/ssl" + +require_var() { + local name="$1" + if [ -z "${!name:-}" ]; then + echo "ERROR: required environment variable ${name} is not set" >&2 + exit 1 + fi +} + +for var in MAIL_DOMAIN MYHOSTNAME \ + POSTGRES_HOST POSTGRES_PORT POSTGRES_USER POSTGRES_PASSWORD POSTGRES_DB \ + SMTP_RELAY_HOST SMTP_RELAY_PORT; do + require_var "$var" +done + +# TLS: use mounted cert/key if provided, otherwise generate a self-signed +# "snakeoil" certificate on first start, matching the upstream docs' approach. +mkdir -p "$SSL_DIR" +if [ -n "${TLS_CERT_FILE:-}" ] && [ -n "${TLS_KEY_FILE:-}" ]; then + export SMTPD_TLS_CERT_FILE="$TLS_CERT_FILE" + export SMTPD_TLS_KEY_FILE="$TLS_KEY_FILE" +else + export SMTPD_TLS_CERT_FILE="${SSL_DIR}/snakeoil.pem" + export SMTPD_TLS_KEY_FILE="${SSL_DIR}/snakeoil.key" + if [ ! -f "$SMTPD_TLS_CERT_FILE" ] || [ ! -f "$SMTPD_TLS_KEY_FILE" ]; then + echo "No TLS_CERT_FILE/TLS_KEY_FILE provided, generating a self-signed certificate" + openssl req -x509 -nodes -days 3650 \ + -newkey rsa:2048 \ + -subj "/CN=${MYHOSTNAME}" \ + -keyout "$SMTPD_TLS_KEY_FILE" \ + -out "$SMTPD_TLS_CERT_FILE" + chmod 600 "$SMTPD_TLS_KEY_FILE" + fi +fi + +render() { + local template="$1" + local destination="$2" + envsubst '${MAIL_DOMAIN} ${MYHOSTNAME} ${SMTPD_TLS_CERT_FILE} ${SMTPD_TLS_KEY_FILE} ${POSTGRES_HOST} ${POSTGRES_PORT} ${POSTGRES_USER} ${POSTGRES_PASSWORD} ${POSTGRES_DB} ${SMTP_RELAY_HOST} ${SMTP_RELAY_PORT}' \ + < "$template" > "$destination" +} + +render "${TEMPLATE_DIR}/main.cf.template" /etc/postfix/main.cf +render "${TEMPLATE_DIR}/pgsql-relay-domains.cf.template" /etc/postfix/pgsql-relay-domains.cf +render "${TEMPLATE_DIR}/pgsql-transport-maps.cf.template" /etc/postfix/pgsql-transport-maps.cf + +# The pgsql lookup config files embed the database password; restrict access. +chown root:postfix /etc/postfix/pgsql-relay-domains.cf /etc/postfix/pgsql-transport-maps.cf +chmod 640 /etc/postfix/pgsql-relay-domains.cf /etc/postfix/pgsql-transport-maps.cf + +postfix check + +exec /usr/sbin/postfix start-fg diff --git a/images/postfix-pgsql/templates/main.cf.template b/images/postfix-pgsql/templates/main.cf.template new file mode 100644 index 0000000..261b281 --- /dev/null +++ b/images/postfix-pgsql/templates/main.cf.template @@ -0,0 +1,39 @@ +# ============================================================================= +# Postfix main.cf template +# +# Rendered at container startup by docker-entrypoint.sh, with values +# substituted from environment variables. See README.md for the full list +# of supported variables. +# ============================================================================= + +myhostname = ${MYHOSTNAME} +mydomain = ${MAIL_DOMAIN} +myorigin = $mydomain +mydestination = +inet_interfaces = all +inet_protocols = ipv4 + +# Dynamic relay/transport decisions backed by PostgreSQL lookup maps. +relay_domains = pgsql:/etc/postfix/pgsql-relay-domains.cf +transport_maps = pgsql:/etc/postfix/pgsql-transport-maps.cf + +# This instance only relays mail for the domains returned by relay_domains; +# it does not accept mail for arbitrary destinations. +smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination +mynetworks = 127.0.0.0/8, [::1]/128 + +# TLS +smtpd_tls_cert_file = ${SMTPD_TLS_CERT_FILE} +smtpd_tls_key_file = ${SMTPD_TLS_KEY_FILE} +smtpd_use_tls = yes +smtpd_tls_security_level = may +smtp_tls_security_level = may + +# Log to stdout/stderr so `docker logs` captures Postfix activity directly, +# without needing a syslog daemon inside the container. +maillog_file = /dev/stdout + +biff = no +append_dot_mydomain = no +readme_directory = no +compatibility_level = 3.6 diff --git a/images/postfix-pgsql/templates/pgsql-relay-domains.cf.template b/images/postfix-pgsql/templates/pgsql-relay-domains.cf.template new file mode 100644 index 0000000..c405409 --- /dev/null +++ b/images/postfix-pgsql/templates/pgsql-relay-domains.cf.template @@ -0,0 +1,12 @@ +# ============================================================================= +# Postfix pgsql_table lookup: relay_domains +# +# Returns the domain itself when Postfix should relay mail for it. +# Rendered at container startup from environment variables. +# ============================================================================= + +user = ${POSTGRES_USER} +password = ${POSTGRES_PASSWORD} +hosts = ${POSTGRES_HOST}:${POSTGRES_PORT} +dbname = ${POSTGRES_DB} +query = SELECT domain FROM relay_domain WHERE domain='%s' diff --git a/images/postfix-pgsql/templates/pgsql-transport-maps.cf.template b/images/postfix-pgsql/templates/pgsql-transport-maps.cf.template new file mode 100644 index 0000000..8548159 --- /dev/null +++ b/images/postfix-pgsql/templates/pgsql-transport-maps.cf.template @@ -0,0 +1,15 @@ +# ============================================================================= +# Postfix pgsql_table lookup: transport_maps +# +# Returns the internal SMTP relay target that accepted mail gets forwarded +# to once a recipient domain matches relay_domains. The target itself is +# baked into the query from SMTP_RELAY_HOST/SMTP_RELAY_PORT at startup; this +# image has no further knowledge of what consumes the mail downstream. +# Rendered at container startup from environment variables. +# ============================================================================= + +user = ${POSTGRES_USER} +password = ${POSTGRES_PASSWORD} +hosts = ${POSTGRES_HOST}:${POSTGRES_PORT} +dbname = ${POSTGRES_DB} +query = SELECT 'smtp:[${SMTP_RELAY_HOST}]:${SMTP_RELAY_PORT}' FROM relay_domain WHERE domain='%s'