PART 3
Step 9 – `docker-compose.yml`:
Code:
# /opt/disclaimer-relay/docker-compose.yml
services:
relay:
build: .
container_name: disclaimer-relay
restart: unless-stopped
network_mode: host
volumes:
- ./dkim/<your-domain>.private:/etc/opendkim/keys/<your-domain>.private:ro
healthcheck:
test: ["CMD-SHELL", "postfix status && test -S /var/spool/postfix/public/pickup"]
interval: 30s
timeout: 5s
retries: 3
`network_mode: host` is deliberate: the container needs to reach `127.0.0.1:2525` and `127.0.0.1:12301`, and binding to `127.0.0.1` inside a bridged network would not be reachable from Plesk's Postfix. With `network_mode: host` it listens on the host's loopback, which is exactly what we want and nothing more.
Step 10 – Entrypoint:
Code:
#!/bin/bash
# /opt/disclaimer-relay/bin/entrypoint.sh
set -e
mkdir -p /run/opendkim /var/spool/postfix
chown -R opendkim:opendkim /run/opendkim /etc/opendkim
# Start OpenDKIM in the background
opendkim -x /etc/opendkim.conf &
sleep 1
# Validate Postfix configuration before starting
postfix check
# Start Postfix in the foreground so the container stays alive
exec postfix start-fg
Code:
chmod 755 /opt/disclaimer-relay/bin/entrypoint.sh
chmod 755 /opt/disclaimer-relay/bin/add-disclaimer
Step 11 – Copy the DKIM private key:
Code:
cp /etc/opendkim/keys/<your-domain>/<your-selector>.private \
/opt/disclaimer-relay/dkim/<your-domain>.private
chmod 600 /opt/disclaimer-relay/dkim/<your-domain>.private
Make sure the file name and selector match what is in `KeyTable` and `opendkim.conf`.
Step 12 – Build and start the relay:
Code:
cd /opt/disclaimer-relay
docker compose build
docker compose up -d
docker compose logs -f relay
Check the logs for:
- `opendkim: ... listening on 127.0.0.1:12301`
- Postfix starting without `warning` or `fatal`
Quick smoke test from the host:
Code:
# Should connect and show the SMTP banner
nc -zv 127.0.0.1 2525
nc -zv 127.0.0.1 10026
Step 13 – Plesk side: the transport map:
This is the only change on the Plesk host, and it is a file that Plesk does not manage. We do not touch `master.cf` at all.
Code:
cat > /etc/postfix/transport-disclaimer <<'EOF'
<your-domain> smtp:[127.0.0.1]:2525
EOF
postmap /etc/postfix/transport-disclaimer
chmod 644 /etc/postfix/transport-disclaimer /etc/postfix/transport-disclaimer.db
Then add the map to `main.cf`. To avoid clobbering an existing `transport_maps` line, do it with `postconf`:
Code:
# Show current value
postconf -h transport_maps
# Append our map (keeps existing entries if any)
CURRENT=$(postconf -h transport_maps)
if [ -z "$CURRENT" ]; then
postconf -e "transport_maps = hash:/etc/postfix/transport-disclaimer"
else
case "$CURRENT" in
*transport-disclaimer*) echo "already present" ;;
*) postconf -e "transport_maps = $CURRENT, hash:/etc/postfix/transport-disclaimer" ;;
esac
fi
postfix check && postfix reload
Step 14 – Self-healing for the transport map (the only Plesk-touchable bit):
Plesk can theoretically rewrite `main.cf` and drop the `transport_maps` line. The map file itself survives, so the fix is trivial and safe. A tiny cron job is enough:
Code:
cat > /usr/local/sbin/plesk-transport-check.sh <<'EOF'
#!/bin/bash
# Re-add the disclaimer transport map if Plesk removed it.
MAP="hash:/etc/postfix/transport-disclaimer"
CURRENT=$(postconf -h transport_maps 2>/dev/null || true)
if printf '%s' "$CURRENT" | grep -q "transport-disclaimer"; then
exit 0
fi
if [ -z "$CURRENT" ]; then
postconf -e "transport_maps = $MAP"
else
postconf -e "transport_maps = $CURRENT, $MAP"
fi
postfix check && postfix reload
logger -t disclaimer-relay "transport_maps was reset by Plesk, re-applied"
EOF
chmod 700 /usr/local/sbin/plesk-transport-check.sh
echo '*/5 * * * * root /usr/local/sbin/plesk-transport-check.sh' \
> /etc/cron.d/plesk-transport-check
chmod 644 /etc/cron.d/plesk-transport-check
This is far less invasive than the `master.cf` surgery from Option 2: it only ever re-adds one `postconf` line, and if it fails, mail still flows (just without the disclaimer), so nothing breaks.
Step 15 – Disable Plesk's DKIM for this domain:
This is important. If Plesk keeps signing outgoing mail for `<your-domain>` before it reaches the relay, the relay's disclaimer invalidates that signature, and you end up with `dkim=fail` again.
Code:
# Turn off Plesk's DKIM signing for the domain
plesk bin dkim_manager -d -domain <your-domain>
The DNS record (`<your-selector>._domainkey.<your-domain>`) stays in place – the relay uses the same key, so external verifiers still find it. Only the *signing* moves from Plesk to the relay.
Double-check that no other DKIM signer is active for this domain (for example the Email Security extension or a milter in Plesk's `main.cf`):
Code:
postconf -h smtpd_milters non_smtpd_milters
If Plesk is signing via a milter for all domains, you may need to scope that down in Plesk's own settings rather than in `main.cf`. In the default Plesk setup, DKIM is done per-domain by Plesk itself, so `plesk bin dkim_manager -d` is enough.
Step 16 – Test:
1. Send a mail from an address in `<your-domain>` to a Gmail account.
2. In Gmail, open the message → "Show original".
3. Check:
- The disclaimer is visible in the body.
- `Authentication-Results` contains `dkim=pass header.d=<your-domain>`.
- `X-Disclaimer-Added: yes` is present.
4. Send a mail from a different domain on the same Plesk server to the same Gmail account and confirm no disclaimer and no `X-Disclaimer-Added` header.
If DKIM fails, check the relay logs:
Code:
docker compose logs --tail=200 relay | grep -Ei 'dkim|opendkim|sign'
Common causes: wrong selector in `KeyTable`/`opendkim.conf`, private key permissions, or Plesk still signing for the domain.
Step 17 – Rollback (if you need it):
Code:
# Remove the transport map entry
CURRENT=$(postconf -h transport_maps)
NEW=$(printf '%s' "$CURRENT" | sed 's#,\?[[:space:]]*hash:/etc/postfix/transport-disclaimer##g')
postconf -e "transport_maps = $NEW"
postfix check && postfix reload
# Remove the cron job
rm -f /etc/cron.d/plesk-transport-check
# Stop and remove the relay
cd /opt/disclaimer-relay && docker compose down
# Re-enable Plesk DKIM if you want it back
plesk bin dkim_manager -e -domain <your-domain>
Why this should be a clean solution?
1. Plesk's Postfix is untouched except for one `transport_maps` line, which is trivially re-addable and whose loss does not break mail flow.
2. DKIM is valid because signing happens in the relay, after the body was modified.
3. Only `<your-domain>` is affected. The transport map matches exactly one domain; everything else goes out exactly as before.
4. Plesk updates cannot break it in any way that matters: even if `main.cf` is rewritten, the cron job restores the one line within 5 minutes, and mail keeps flowing meanwhile.
5. No second server needed. The relay runs on the same host, isolated in Docker, bound to `127.0.0.1` only.
6. MIME safety. alterMIME only touches `text/plain` and `text/html`, so inline images and calendar invites survive.
7. You are now running a
second Postfix on the host. Keep it updated (`docker compose build --pull` and `docker compose up -d` on a regular schedule, e.g. monthly, or via a cron job). A stale relay is a security risk, just like a stale Plesk.
8. Outbound port 25. The relay sends mail directly to the internet on port 25. If your host or provider blocks it, set `relayhost` in the relay's `main.cf` to your provider's smarthost and add SASL credentials – Plesk's own outbound settings do **not** apply to the container.
9. No greylisting / no Plesk spam checks on the relay. The relay is a pass-through, so it does not need them. But if you ever route inbound mail through it, that would change – don't do that. The transport map only matches outgoing mail for one domain, and that is the intended scope.
10. The disclaimer is added to all outgoing mail from `<your-domain>`, including internal mail between two mailboxes on the same server. If that is not what you want, add a recipient check in `add-disclaimer` (e.g. skip if `$RECIPIENT` matches a local mailbox).
11. DKIM key rotation. If you ever rotate the DKIM key in Plesk, you must copy the new private key into `/opt/disclaimer-relay/dkim/` and restart the container. Add this to your runbook.
12. Test first. Set this up on a test domain, send to Gmail, Outlook and a self-hosted recipient, and check `Authentication-Results` in all three before you touch production.
13. Monitoring. Watch the relay logs and the cron job's `logger` tag `disclaimer-relay`. If the relay is down, mail for `<your-domain>` will queue in Plesk's Postfix (it does not get lost), but it will not go out until the relay is back. A simple check like `docker compose ps` in your monitoring is enough.
Let me know how the test goes.
And please, no comments about whether I had a lot of free time. There was a bit of self-interest involved, because I need something like this myself.
Regards
Tessa