Postfix on Kubernetes is less about a single "deployment" and more about orchestrating a distributed, resilient email delivery system.

Let’s watch Postfix do its thing.

# Simulate an incoming email delivery attempt
kubectl exec -it <pod-name> -- \
  swaks --to alice@example.com --from bob@example.com \
  --server localhost:25 --data - <<EOF
Subject: Test Email
From: bob@example.com
To: alice@example.com

This is the body of the test email.
EOF

# Observe Postfix logs for the delivery attempt
kubectl logs <pod-name> -f | grep "delivery to"

The core problem Postfix solves on Kubernetes is transforming the ephemeral, cattle-like nature of pods into a stable, available mail server. This means handling persistent storage for mail queues and logs, ensuring network reachability, managing TLS certificates, and dealing with the inherent challenges of sending email from dynamic IP addresses.

At its heart, Postfix in Kubernetes relies on a few key components:

  1. StatefulSet: This is your go-to for running Postfix. Unlike Deployments, StatefulSets provide stable network identifiers, persistent storage, and ordered, graceful deployment and scaling. Each Postfix pod gets a stable hostname (e.g., postfix-0, postfix-1) and its own persistent volume for queues and logs.
  2. PersistentVolumeClaims (PVCs): These define the storage requirements for your Postfix pods. You’ll need at least one PVC for mail queues (where emails wait to be sent or are received) and potentially another for logs.
  3. Service: A ClusterIP or LoadBalancer service makes your Postfix instance accessible within the cluster and, if using LoadBalancer, from the outside world. For outbound mail, you might use an external SMTP relay, which simplifies IP reputation management. For inbound, this service would receive mail from other MTAs.
  4. ConfigMap: This holds your main.cf and master.cf configurations. You’ll mount this as a volume into your Postfix pods.
  5. Secrets: For sensitive information like TLS certificates or credentials for an external relay.
  6. NetworkPolicy: Crucial for security, defining which pods can communicate with your Postfix service.

Here’s a simplified StatefulSet definition for Postfix:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postfix
spec:
  serviceName: postfix-service
  replicas: 1
  selector:
    matchLabels:
      app: postfix
  template:
    metadata:
      labels:
        app: postfix
    spec:
      containers:
      - name: postfix
        image: postx/postfix:latest # Or your preferred Postfix image
        ports:
        - containerPort: 25
          name: smtp
        - containerPort: 587
          name: submission
        - containerPort: 465
          name: smtps
        volumeMounts:
        - name: postfix-config
          mountPath: /etc/postfix
        - name: mail-queue
          mountPath: /var/spool/postfix
      volumes:
      - name: postfix-config
        configMap:
          name: postfix-configmap
  volumeClaimTemplates:
  - metadata:
      name: mail-queue
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

And a corresponding ConfigMap for main.cf:

apiVersion: v1
kind: ConfigMap
metadata:
  name: postfix-configmap
data:
  main.cf: |
    # Basic Postfix Configuration
    myhostname = mail.example.com
    mydomain = example.com
    myorigin = $mydomain
    inet_interfaces = all
    # For outbound mail, consider using an smarthost
    # relayhost = smtp.external-relay.com:587
    # For inbound mail, you'll need to configure MX records pointing to your LoadBalancer
    # tls_random_seed_file = /etc/ssl/random-seed
    # smtp_tls_security_level = may
    # smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
    # smtpd_tls_security_level = may
    # smtpd_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
    # smtpd_tls_cert_file = /etc/ssl/private/tls.crt
    # smtpd_tls_key_file = /etc/ssl/private/tls.key
    # smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
    # smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
    # mailbox_size_limit = 0
    # recipient_delimiter = +
    # home_mailbox = Maildir/

    # For a robust setup, you'll need more advanced configurations for TLS,
    # spam filtering, rate limiting, etc.

The key to making Postfix resilient on Kubernetes is understanding how its internal queues (incoming, active, deferred, bounce, maildrop) are managed. By using StatefulSet and PersistentVolumeClaims, each pod has its own dedicated storage for these queues, ensuring that if a pod restarts, its mail processing state isn’t lost.

When you’re setting up Postfix to receive mail, the most counterintuitive part is that your Kubernetes Service of type LoadBalancer typically gets assigned an ephemeral IP address. This means you cannot reliably point your domain’s MX records directly to it. Instead, you’ll usually configure your MX records to point to a dedicated, static IP address from your cloud provider or a third-party mail delivery service, which then forwards mail to your Kubernetes cluster’s LoadBalancer IP or a specific node port. This indirection is essential for maintaining stable inbound mail flow.

The next hurdle is often configuring Postfix to send outbound mail without getting blacklisted.

Want structured learning?

Take the full Smtp course →