All sourced from 2026-04-17 work sessions: - fail2ban-nginx-bad-request-jail: enable stock jail (just needs wiring) - fail2ban-apache-bad-request-jail: custom filter from scratch, no stock equivalent - ssh-hardening-ansible-fleet: drop-in approach with Fedora/Ubuntu edge cases - watchtower-smtp-localhost-relay: credential-free localhost postfix relay pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.8 KiB
title, domain, category, tags, status, created, updated
| title | domain | category | tags | status | created | updated | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Watchtower SMTP via Localhost Postfix Relay | selfhosting | docker |
|
published | 2026-04-17 | 2026-04-17 |
Watchtower SMTP via Localhost Postfix Relay
The Problem
Watchtower supports email notifications via its built-in shoutrrr SMTP driver. The typical setup stores SMTP credentials in the compose file or a separate env file. This creates two failure modes:
- Password rotation breaks notifications silently. When you rotate your mail server password, Watchtower keeps running but stops sending emails. You only discover it when you notice container updates happened with no notification.
- Credentials at rest.
docker-compose.ymland.envfiles are often world-readable or checked into git. SMTP passwords stored there are a credential leak waiting to happen.
The shoutrrr SMTP driver also has a quirk: it attempts AUTH over an unencrypted connection to remote SMTP servers, which most mail servers (correctly) reject with 535 5.7.8 authentication failed or similar.
The Solution
Route Watchtower's outbound mail through localhost port 25 using network_mode: host. The local Postfix MTA — already running on the host for relay purposes — handles authentication to the upstream mail server. Watchtower never sees a credential.
Watchtower → localhost:25 (Postfix, trusted via mynetworks — no auth required)
→ Postfix → upstream mail server → delivery
docker-compose.yml
services:
watchtower:
image: containrrr/watchtower
restart: always
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DOCKER_API_VERSION=1.44
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_INCLUDE_STOPPED=false
- WATCHTOWER_NOTIFICATIONS=email
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=watchtower@yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_TO=you@yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=localhost
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=25
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY=true
- WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2
Key settings:
network_mode: host— required solocalhostresolves to the host's loopback interface (and port 25). Without this,localhostresolves to the container's own loopback, which has no Postfix.EMAIL_SERVER=localhost,PORT=25— target the local PostfixTLS_SKIP_VERIFY=true— shoutrrr still negotiates STARTTLS even on port 25; a self-signed or expired local Postfix cert is fine to skip- No
EMAIL_SERVER_USERorEMAIL_SERVER_PASSWORD— Postfix trusts127.0.0.1viamynetworks, no auth needed
Prerequisites
The host needs a Postfix instance that:
- Listens on
localhost:25 - Includes
127.0.0.0/8inmynetworksso local processes can relay without authentication - Is configured to relay outbound to your actual mail server
This is standard for any host already running a Postfix relay. If Postfix isn't installed, a minimal relay-only config is a few lines in main.cf.
Why Not Just Use an Env File?
A separate env file (mode 0600) is better than inline compose, but you still have a credential that breaks on rotation. The localhost relay pattern eliminates the credential entirely.
| Approach | Credentials stored | Rotation-safe |
|---|---|---|
| Inline in compose | Yes (plaintext, often 0644) | ❌ |
| Separate env file (0600) | Yes (protected but present) | ❌ |
| Localhost Postfix relay | None | ✅ |
Testing
After docker compose up -d, check the Watchtower logs for a startup notification:
docker logs <watchtower-container-name> 2>&1 | head -20
# Look for: "Sending notification..."
Confirm Postfix delivered it:
grep watchtower /var/log/mail.log | tail -5
# Look for: status=sent (250 2.0.0 Ok)
Gotchas
network_mode: hostis Linux-only. Docker Desktop on macOS/Windows doesn't support host networking. This pattern only works on Linux hosts.network_mode: hostdrops port mappings. Anyports:entries are silently ignored undernetwork_mode: host. Watchtower doesn't expose ports, so this isn't an issue.- Postfix TLS cert warning. shoutrrr attempts STARTTLS on port 25 regardless. If the local Postfix has a self-signed or expired cert,
TLS_SKIP_VERIFY=truesuppresses the error. For a proper fix, renew the Postfix cert. WATCHTOWER_DISABLE_CONTAINERS. If you run stacks that manage their own updates (Nextcloud AIO, etc.), list those containers here (space-separated) to prevent Watchtower from interfering.