Postfix Relay in Docker installieren

Wenn du ein Postfix Relay Docker Setup suchst, willst du meistens genau eine Sache: Geräte und Dienste aus deinem eigenen Netz sollen Mails verschicken können, ohne dass du in jedem Drucker, Monitoring-Tool oder Script SMTP-Auth-Zugangsdaten hinterlegen musst. Der Relay nimmt die Mail intern an und versendet sie anschließend über deinen richtigen Mailserver weiter. Also über den Server, der SPF, DKIM, DMARC und den ganzen E-Mail-Zirkus sauber beherrscht.

In diesem Beitrag zeige ich dir, wie du einen kleinen Postfix SMTP-Relay in Docker baust. Ich orientiere mich dabei an einem typischen Setup: intern dürfen definierte Netze ohne Auth einliefern, nach außen geht alles über einen richtigen Mailserver auf Port 587 mit STARTTLS und SMTP-Auth. Die Zugangsdaten bleiben natürlich Platzhalter. So viel Abenteuer muss dann doch nicht sein.

🛠 Voraussetzungen

Bevor es losgeht, brauchst du eine laufende Docker-Umgebung. Wenn Docker bei dir noch nicht installiert ist, findest du hier meine passende Schritt-für-Schritt-Anleitung:

Außerdem brauchst du einen Mailserver, über den der Relay später wirklich versenden darf. Im Beispiel nutze ich dafür mail.example.de auf Port 587 mit STARTTLS. Bei dir trägst du dort natürlich deinen eigenen Mailserver ein.

📬 Warum überhaupt ein Postfix Relay?

Ein Relay-Server ist praktisch, wenn mehrere interne Systeme Mails verschicken sollen. Typische Kandidaten sind Monitoring, Backup-Scripts, NAS-Systeme, Drucker, Scanner, Firewalls oder kleine Tools, die zwar E-Mails senden können, aber bei moderner SMTP-Auth eher aussehen, als hätten sie zuletzt 2008 ein Update gesehen.

Statt überall Benutzername, Passwort, Port, TLS-Modus und Absenderadresse zu pflegen, trägst du bei den internen Systemen nur den Relay ein. Der Relay nimmt Mails aus erlaubten Netzen an und kümmert sich zentral um den Versand nach draußen.

  • Innen: Geräte aus deinen erlaubten Netzen dürfen Mails ohne Auth an den Relay schicken.
  • Außen: Der Relay meldet sich mit SMTP-Auth bei deinem echten Mailserver an.
  • Zentral: Du pflegst Zugangsdaten, Absenderregeln und Logs an einer Stelle.
  • Kontrolliert: Du erlaubst nur definierte Netze. Sonst baust du aus Versehen ein offenes Relay, und das willst du wirklich nicht.

🧱 Was wir bauen

Wir bauen einen kleinen Docker-Container auf Basis von Ubuntu und Postfix. Die Konfiguration liegt persistent auf dem Host, damit du sie sichern, ändern und nachvollziehen kannst. Der Container bekommt außerdem eine feste IP in einem eigenen Docker-Netzwerk.

Im Beispiel verwende ich diese Werte:

  • Container: postfix-relay
  • Docker-Netzwerk: postfix-relay.dockernetwork.local
  • Container-IP: 172.29.64.10
  • SMTP im Container: Port 25
  • Optionaler Auth-Listener im Container: Port 2525
  • Relayhost nach außen: [mail.example.de]:587

Das Subnetz kannst du ändern. Wichtig ist nur: Nimm ein privates Netz, das auf deinem Docker-Host noch nicht verwendet wird.

📁 Projektordner vorbereiten

Ich lege Docker-Projekte gerne sauber unter einem eigenen Ordner ab. Für den Relay sieht das so aus:

mkdir -p /home/docker-container/postfix-relay/data/postfix
mkdir -p /home/docker-container/postfix-relay/data/spool/postfix
mkdir -p /home/docker-container/postfix-relay/data/mail
mkdir -p /home/docker-container/postfix-relay/data/log
cd /home/docker-container/postfix-relay

Die Ordner sind bewusst getrennt. Postfix-Konfiguration, Queue, lokale Maildaten und Logs liegen damit nicht wild durcheinander. Das freut später auch dein Backup.

🐳 Dockerfile für Postfix

Postfix ist kein typischer Ein-Prozess-Webcontainer. Deshalb bekommt der Container ein kleines Startscript und ein paar Hilfspakete. Wichtig sind hier unter anderem postfix, libsasl2-modules, postfix-pcre, rsyslog, rsync und tini.

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Europe/Berlin

RUN set -eux; \
    echo "postfix postfix/mailname string localhost" | debconf-set-selections; \
    echo "postfix postfix/main_mailer_type select No configuration" | debconf-set-selections; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
      ca-certificates \
      ed \
      libsasl2-modules \
      mailutils \
      netcat-openbsd \
      postfix \
      postfix-pcre \
      rsync \
      rsyslog \
      sasl2-bin \
      tini; \
    sed -ri 's/^([[:space:]]*module\(load="imklog"[^)]*\).*)/# \1/' /etc/rsyslog.conf; \
    sed -ri 's/^([[:space:]]*\$ModLoad[[:space:]]+imklog.*)/# \1/' /etc/rsyslog.conf; \
    mkdir -p /opt/postfix-spool-template /opt/postfix-config-template; \
    rsync -a /var/spool/postfix/ /opt/postfix-spool-template/; \
    rsync -a /etc/postfix/ /opt/postfix-config-template/; \
    rm -rf /var/lib/apt/lists/*

COPY entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod 0755 /usr/local/bin/entrypoint.sh

ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
CMD ["postfix", "start-fg"]

Der Teil mit /opt/postfix-config-template ist wichtig. Wenn du später /etc/postfix als Volume einhängst, verdeckt Docker die Standarddateien aus dem Image. Das Startscript kann fehlende Dateien dann aus der Template-Kopie nachziehen. Ohne diesen Schritt stolpert man schnell über fehlende Einträge in der master.cf. Einmal erlebt, nie wieder vergessen.

🚪 EntryPoint anlegen

Das Startscript initialisiert die Postfix-Dateien, erzeugt die Hash-Maps mit postmap, startet rsyslog und führt Postfix im Vordergrund aus.

nano entrypoint.sh
#!/bin/sh
set -eu

initialize_spool() {
  if [ ! -d /var/spool/postfix/private ] || [ ! -d /var/spool/postfix/public ]; then
    rsync -a /opt/postfix-spool-template/ /var/spool/postfix/
  fi
}

initialize_config() {
  rsync -a --ignore-existing /opt/postfix-config-template/ /etc/postfix/
}

postmap_if_present() {
  map_file="$1"
  if [ -f "$map_file" ]; then
    postmap "$map_file"
  fi
}

sync_chroot_resolver() {
  mkdir -p /var/spool/postfix/etc
  for file in hosts resolv.conf nsswitch.conf services protocols; do
    if [ -f "/etc/$file" ]; then
      cp -a "/etc/$file" "/var/spool/postfix/etc/$file"
    fi
  done
}

initialize_config
initialize_spool
sync_chroot_resolver
postfix upgrade-configuration >/dev/null 2>&1 || true

mkdir -p /var/log /var/mail
touch /var/log/mail.log /etc/aliases

if [ -n "${POSTFIX_MYHOSTNAME:-}" ]; then
  postconf -e "myhostname = ${POSTFIX_MYHOSTNAME}"
fi

if [ -n "${POSTFIX_RELAYHOST:-}" ]; then
  postconf -e "relayhost = ${POSTFIX_RELAYHOST}"
fi

if [ -n "${POSTFIX_MYNETWORKS:-}" ]; then
  postconf -e "mynetworks = ${POSTFIX_MYNETWORKS}"
fi

postconf -e "maillog_file = /var/log/mail.log"

if [ -f /etc/postfix/sasl_passwd ]; then
  chmod 0600 /etc/postfix/sasl_passwd
fi

if [ -f /etc/sasldb2 ]; then
  chown postfix:postfix /etc/sasldb2
  chmod 0640 /etc/sasldb2
fi

postmap_if_present /etc/postfix/sasl_passwd
postmap_if_present /etc/postfix/sender_login_maps
postmap_if_present /etc/postfix/transport
newaliases

postfix check

if command -v rsyslogd >/dev/null 2>&1; then
  rsyslogd || true
fi

exec "$@"

Danach machst du das Script ausführbar:

chmod +x entrypoint.sh

⚙️ Postfix Relay Docker konfigurieren

Jetzt kommt die eigentliche Postfix-Konfiguration. Lege die Datei data/postfix/main.cf an:

nano data/postfix/main.cf
smtpd_banner = $myhostname ESMTP $mail_name (Docker)
biff = no
append_dot_mydomain = no
compatibility_level = 2

myhostname = postfix-relay.example.lan
mydestination = $myhostname, localhost.$mydomain, localhost
relayhost = [mail.example.de]:587
mynetworks = 127.0.0.0/8 10.0.1.0/24 172.29.64.0/24
mynetworks_style = subnet
inet_protocols = ipv4
recipient_delimiter = +

alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
maillog_file = /var/log/mail.log

smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd

smtp_generic_maps = regexp:/etc/postfix/generic
sender_canonical_maps = regexp:/etc/postfix/sender_canonical
transport_maps = hash:/etc/postfix/transport

smtpd_recipient_restrictions =
  permit_mynetworks
  reject_unauth_destination

Der wichtigste Teil ist mynetworks. Dort stehen nur Netze, denen du wirklich vertraust. In meinem Setup sind das zum Beispiel interne Netze und die Docker-Bridge des Relay-Containers. Alles andere wird nicht einfach so weitergeleitet.

Mit relayhost sagst du Postfix, wohin ausgehende Mails gehen sollen. Für mein Setup sieht die relevante Zeile so aus:

relayhost = [mail.example.de]:587

Die eckigen Klammern sind Absicht. Damit verhindert Postfix, dass für den Host noch einmal MX-Records aufgelöst werden. Er soll genau diesen Mailserver verwenden.

SMTP-Auth für den echten Mailserver

Die SMTP-Zugangsdaten für den ausgehenden Mailserver kommen in data/postfix/sasl_passwd:

nano data/postfix/sasl_passwd
[mail.example.de]:587 smtp-benutzer@example.de:DEIN_PASSWORT

Für mein Setup wäre das sinngemäß:

[mail.example.de]:587 relay-user@example.de:DEIN_PASSWORT

Die Datei sollte nur für root lesbar sein:

chmod 600 data/postfix/sasl_passwd

Beim Containerstart macht das EntryPoint-Script daraus automatisch die passende Hash-Datei mit postmap. Wenn du die Datei später im laufenden Betrieb änderst, musst du postmap erneut ausführen oder den Container neu starten.

Absenderadresse vereinheitlichen

Viele interne Systeme senden gerne als root@hostname, monitoring@server oder irgendetwas ähnlich Schönes. Für den Versand nach außen ist das meistens keine gute Idee. Deshalb kannst du lokale Absender auf eine saubere Adresse umschreiben.

nano data/postfix/generic
/^root(@.*)?$/      noreply@example.de
/^postfix(@.*)?$/   noreply@example.de

Wenn dein Mailserver nur bestimmte Absender erlaubt, ist das besonders wichtig. Sonst nimmt dein Relay die Mail zwar an, aber der eigentliche Mailserver lehnt sie später ab.

Transport-Map optional vorbereiten

Mit einer Transport-Map kannst du einzelne Ziele anders routen. Für einen einfachen Relay brauchst du das nicht zwingend. Ich lege die Datei trotzdem gerne an, weil Postfix dann beim Start nichts vermisst und spätere Sonderfälle schnell ergänzt sind.

nano data/postfix/transport
# Optional: einzelne Ziele anders routen.
# example.net smtp:[smtp.example.net]:25

🧾 Docker Compose Datei

Jetzt kommt der eigentliche Docker-Stack. Jeder Container bekommt eine feste IP. In diesem Beispiel ist es nur ein Container, aber die Struktur bleibt sauber und nachvollziehbar.

---
services:
  postfix-relay:
    build:
      context: .
    image: local/postfix-relay:2026-08-26
    container_name: postfix-relay
    hostname: postfix-relay.example.lan
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "25:25/tcp"
    volumes:
      - ./data/postfix:/etc/postfix
      - ./data/aliases:/etc/aliases
      - ./data/spool/postfix:/var/spool/postfix
      - ./data/mail:/var/mail
      - ./data/log:/var/log
    healthcheck:
      test: ["CMD-SHELL", "postfix status >/dev/null 2>&1 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    networks:
      postfix_relay_net:
        ipv4_address: 172.29.64.10

networks:
  postfix_relay_net:
    name: postfix-relay.dockernetwork.local
    driver: bridge
    ipam:
      config:
        - subnet: 172.29.64.0/24
          gateway: 172.29.64.1
          ip_range: 172.29.64.128/25

Im Grundsetup mappe ich Host-Port 25 auf Container-Port 25. In produktiven Umgebungen binde ich solche Ports gerne gezielt an die interne Server-IP, zum Beispiel:

ports:
  - "192.168.1.10:25:25/tcp"

Damit lauscht der Relay nicht blind auf allen Interfaces. Das ist bei Maildiensten eine sehr gesunde Angewohnheit.

Optional: Port 2525 mit SMTP-Auth

Manchmal hast du interne Geräte oder Dienste, die unbedingt SMTP-Auth verwenden wollen. Reolink ist so ein Kandidat. Für solche Fälle kannst du Port 2525 zusätzlich öffnen und dort Auth erzwingen, während Port 25 weiterhin für deine anderen internen Services ohne Auth funktioniert.

Das Prinzip ist dann:

  • 25: interne Clients aus mynetworks, Versand ohne Auth.
  • 2525: interne Clients mit Benutzername und Passwort.

Wichtig: Auch Port 2525 gehört nicht offen ins Internet. Wenn ein Gerät darüber Auth macht, ist das trotzdem ein interner Dienst und sollte per Firewall entsprechend begrenzt sein.

Für den Auth-Port brauchst du zusätzlich eine persistente SASL-Datenbank. Lege die Datei vor dem ersten Start an, sonst macht Docker daraus unter Umständen ein Verzeichnis. Das ist dann wieder so ein schöner Moment, in dem man kurz an seinem Beruf zweifelt.

touch data/sasldb2
chmod 640 data/sasldb2

Dann ergänzt du im Compose-File den zweiten Port und die SASL-Datenbank:

ports:
  - "192.168.1.10:25:25/tcp"
  - "192.168.1.10:2525:2525/tcp"

volumes:
  - ./data/postfix:/etc/postfix
  - ./data/aliases:/etc/aliases
  - ./data/sasldb2:/etc/sasldb2
  - ./data/spool/postfix:/var/spool/postfix
  - ./data/mail:/var/mail
  - ./data/log:/var/log

Damit Postfix Benutzer aus der SASL-Datenbank prüfen kann, legst du die Datei data/postfix/sasl/smtpd.conf an:

mkdir -p data/postfix/sasl
nano data/postfix/sasl/smtpd.conf
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN

Danach brauchst du eine Zuordnung, welche Absenderadresse zu welchem Login passt:

nano data/postfix/sender_login_maps
relay@example.de smtpauth

Den zusätzlichen Listener trägst du in data/postfix/master.cf ein. Die Datei wird beim ersten Containerstart aus dem Image nach data/postfix kopiert. Starte den Container also einmal, stoppe ihn wieder und ergänze dann am Ende der Datei diesen Block:

2525      inet  n       -       n       -       -       smtpd
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=cyrus
  -o smtpd_sasl_path=smtpd
  -o smtpd_tls_security_level=none
  -o smtpd_tls_auth_only=no
  -o smtpd_sender_login_maps=hash:/etc/postfix/sender_login_maps
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject_unauth_destination,reject_sender_login_mismatch
  -o smtpd_helo_required=yes
  -o disable_vrfy_command=yes

Jetzt startest du den Container und legst den Benutzer an:

docker compose up -d --build
docker exec -it postfix-relay saslpasswd2 -c -u postfix-relay.example.lan smtpauth
docker exec postfix-relay postmap /etc/postfix/sender_login_maps
docker exec postfix-relay postfix reload

Im Client verwendest du dann als SMTP-Server deinen Relay, als Port 2525, als Benutzer smtpauth und das Passwort, das du mit saslpasswd2 vergeben hast. Der Client authentifiziert sich also am Relay. Der Relay selbst schickt die Mail danach weiterhin über deinen richtigen Mailserver raus.

Umgebungsvariablen

Die .env ist optional, aber praktisch. Darüber kannst du Hostname, Relayhost und erlaubte Netze anpassen, ohne direkt die main.cf zu bearbeiten.

POSTFIX_MYHOSTNAME=postfix-relay.example.lan
POSTFIX_RELAYHOST=[mail.example.de]:587
POSTFIX_MYNETWORKS=127.0.0.0/8 10.0.1.0/24 172.29.64.0/24

🚀 Stack starten

Wenn alle Dateien liegen, startest du den Stack:

docker compose up -d --build
docker compose ps

Auf meinem Testsystem sah das anschließend so aus:

NAME            IMAGE                            STATUS                    PORTS
postfix-relay   local/postfix-relay:2026-08-26   Up 45 seconds (healthy)   0.0.0.0:25->25/tcp

Die feste Container-IP kannst du ebenfalls prüfen:

docker inspect postfix-relay --format 'IP={{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
IP=172.29.64.10

🧪 Relay testen

Als erstes prüfst du, ob Postfix überhaupt antwortet:

printf quit | nc -w 3 127.0.0.1 25
220 postfix-relay.example.lan ESMTP Postfix (Docker)

Danach kannst du eine Testmail einliefern. Das testet erst einmal die Annahme durch den Relay. Ob der echte Versand nach außen klappt, hängt anschließend von Relayhost, Zugangsdaten, Absenderadresse und DNS ab.

{
  printf 'EHLO server.local\r\n'
  printf 'MAIL FROM:<monitoring@example.lan>\r\n'
  printf 'RCPT TO:<admin@example.net>\r\n'
  printf 'DATA\r\n'
  printf 'Subject: Postfix Relay Test\r\n\r\nDas ist ein lokaler Test.\r\n.\r\n'
  printf 'QUIT\r\n'
} | nc -w 5 127.0.0.1 25

Bei mir wurde die Mail sauber angenommen:

250 2.1.0 Ok
250 2.1.5 Ok
354 End data with <CR><LF>.<CR><LF>
250 2.0.0 Ok: queued as 9EE528BD30
221 2.0.0 Bye

Die Queue prüfst du so:

docker exec postfix-relay postqueue -p

Wenn alles raus ist, sieht es so aus:

Mail queue is empty

🔐 Kein offenes Relay bauen

Der wichtigste Sicherheitspunkt ist simpel: Öffne diesen Relay nicht für das Internet. Ein SMTP-Relay, das von überall ohne Auth annimmt, wird früher oder später missbraucht. Wahrscheinlich früher.

Achte besonders auf diese Punkte:

  • mynetworks enthält nur deine internen Netze und wirklich notwendige Einzel-IP-Adressen.
  • Der Docker-Port ist möglichst nur an eine interne IP gebunden.
  • Die Firewall erlaubt Port 25 oder 2525 nur aus deinen internen Netzen.
  • Der Relayhost nach außen nutzt STARTTLS und SMTP-Auth.
  • Absenderadressen passen zu deiner Domain und sind auf dem Mailserver erlaubt.

Wenn du unsicher bist, teste von einem fremden Netz aus, ob dein Relay ablehnt. Von außen sollte ohne Auth nichts relayen können.

🧯 Typische Fehler

Relay access denied

Dann kommt der Client sehr wahrscheinlich nicht aus einem Netz, das in mynetworks erlaubt ist. Prüfe die Quell-IP aus Sicht des Containers und passe die Netze nur dann an, wenn die Quelle wirklich vertrauenswürdig ist.

Authentication failed beim Versand

Dann stimmen Benutzername, Passwort oder der Eintrag in sasl_passwd nicht. Wichtig: Der Host in sasl_passwd muss exakt zum relayhost passen, also inklusive eckiger Klammern und Port.

relayhost = [mail.example.de]:587

# sasl_passwd
[mail.example.de]:587 smtp-benutzer@example.de:DEIN_PASSWORT

Änderungen greifen nicht

Wenn du Maps wie sasl_passwd oder transport änderst, musst du die Hash-Dateien neu erzeugen und Postfix neu laden:

docker exec postfix-relay postmap /etc/postfix/sasl_passwd
docker exec postfix-relay postmap /etc/postfix/transport
docker exec postfix-relay postfix reload

Oder du startest den Container neu. Das ist nicht elegant, aber ehrlich gesagt bei einem kleinen Relay oft völlig ausreichend.

✅ Fazit

Ein Postfix Relay in Docker ist kein Hexenwerk, aber du solltest sauber trennen: intern dürfen nur definierte Netze einliefern, extern versendet dein Relay über den richtigen Mailserver mit Auth und TLS. Genau dadurch bleibt das Setup bequem für interne Geräte, ohne dass du dir ein offenes Relay ins Netz stellst.

Ich mag diese Lösung besonders für Monitoring, Benachrichtigungen und ältere Geräte. Du hast zentrale Logs, eine zentrale Absenderlogik und musst SMTP-Zugangsdaten nicht überall verteilen. Das ist im Alltag angenehm unspektakulär. Und bei Infrastruktur ist angenehm unspektakulär meistens ein sehr gutes Zeichen.

👥 Techniverse Community

Lust auf Austausch rund um Docker, Selfhosting und andere smarte IT-Lösungen?
In der Techniverse Community triffst du Gleichgesinnte, kannst Fragen stellen oder einfach nerdigen Talk genießen.

👉 Jetzt der Gruppe auf Matrix beitreten
~ Direkte Raumadresse: #community:techniverse.net

👉 Für lockere Gespräche abseits der Kernthemen komm in den Talkraum
~ Direkte Raumadresse: #talk:techniverse.net

Wir freuen uns, wenn du dabei bist!

Vielen Dank fürs Teilen!