Compare commits

..

3 Commits

7 changed files with 296 additions and 0 deletions

View File

@@ -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"]

View File

@@ -0,0 +1 @@
12-slim

View File

@@ -0,0 +1,129 @@
# 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).
## 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.:
```bash
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`
```bash
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
```yaml
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:
1. Edit `POSTFIX_VERSION` with the desired Debian tag.
2. Commit and push.
3. The CI builds the image with that base and tags it with both `latest` and
the value of `POSTFIX_VERSION`.
## Reference
- Upstream Postfix + PostgreSQL setup this image is based on:
https://github.com/simple-login/app

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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'