Watchtower: Migrate Email Notifications to WATCHTOWER_NOTIFICATION_URL
The original Watchtower project (containrrr/watchtower) is archived. Development continues in a fork, nicholas-fedor/watchtower, with its own image (nickfedor/watchtower) and documentation. The fork works as a drop-in replacement — with one catch for anyone who configured email notifications years ago and hasn't touched them since.
The familiar block of WATCHTOWER_NOTIFICATION_EMAIL_* environment variables is deprecated and scheduled for removal in v2. Its replacement is a single variable, WATCHTOWER_NOTIFICATION_URL, which packs the entire SMTP configuration into one shoutrrr URL. The migration is mechanical, but two details break silently if you get them wrong: the encryption parameters and the URL-encoding of your password.
Pre-requisites
- Watchtower running via Docker Compose with the legacy email variables
- The fork's image (
nickfedor/watchtower) — the conversion target is its URL format, documented in the notifications overview
The New Format
One URL replaces seven variables:
| Legacy variable | Goes to |
|---|---|
WATCHTOWER_NOTIFICATION_EMAIL_SERVER |
host |
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT |
port |
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER |
user (URL-encoded) |
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD |
password (URL-encoded) |
WATCHTOWER_NOTIFICATION_EMAIL_FROM |
fromaddress= parameter |
WATCHTOWER_NOTIFICATION_EMAIL_TO |
toaddresses= parameter |
WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY |
skiptlsverify=yes parameter |
WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG |
WATCHTOWER_NOTIFICATION_TITLE_TAG (stays a variable) |
WATCHTOWER_NOTIFICATION_EMAIL_DELAY |
WATCHTOWER_NOTIFICATIONS_DELAY (stays a variable) |
WATCHTOWER_NOTIFICATIONS=email |
delete — the URL itself selects the service |
The encryption and usestarttls parameters have no legacy counterpart; the old notifier picked a TLS mode on its own. Now you state it explicitly, and the right combination follows from the port:
| Port | Parameters |
|---|---|
587 (submission) |
encryption=ExplicitTLS&usestarttls=yes |
465 (SMTPS) |
encryption=ImplicitTLS&usestarttls=no |
25 (plain, LAN relay) |
encryption=None&usestarttls=no |
These pairings match the common SMTP configurations in the official docs. Mixing them up — say, ImplicitTLS on 587 — produces connection timeouts, not a clear error message.
URL-Encoding: Where Migrations Actually Fail
Credentials now live inside a URL, so every character with a meaning in URLs must be percent-encoded. This is the step people skip, and the failure is nasty: a password containing : gets truncated at the colon, because : is the separator between user and password. Authentication then fails with a correct-looking configuration.
Characters that commonly appear in mail passwords:
| Character | Encoded |
|---|---|
@ |
%40 |
: |
%3A |
| space | %20 |
! |
%21 |
& |
%26 |
# |
%23 |
+ |
%2B |
The username needs the same treatment — SMTP logins are usually mail addresses, so the @ becomes %40.
Before and After
A typical legacy setup:
services:
watchtower:
image: nickfedor/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=Europe/Berlin
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_NOTIFICATIONS=email
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=no-reply@example.com
- WATCHTOWER_NOTIFICATION_EMAIL_TO=admin@example.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=mail.example.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=no-reply@example.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=S3cret! P@ss:1
- WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG=Homelab
command: --schedule "0 0 6 * * 1"
restart: always
The same configuration in the new format:
services:
watchtower:
image: nickfedor/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=Europe/Berlin
- WATCHTOWER_CLEANUP=true
- "WATCHTOWER_NOTIFICATION_URL=smtp://no-reply%40example.com:S3cret%21%20P%40ss%3A1@mail.example.com:587/?fromaddress=no-reply%40example.com&toaddresses=admin%40example.com&encryption=ExplicitTLS&usestarttls=yes"
- "WATCHTOWER_NOTIFICATION_TITLE_TAG=Homelab"
command: --schedule "0 0 6 * * 1"
restart: always
Note what happened to the password S3cret! P@ss:1: it became S3cret%21%20P%40ss%3A1. Every special character survived, but only because it was encoded.
Quote the URL line
Wrap the WATCHTOWER_NOTIFICATION_URL entry in double quotes. The value contains & and ?, and while YAML usually tolerates them in plain scalars, quoting removes the ambiguity — a silently mis-parsed URL costs you a debugging session.
While you're in there: check the LEVEL variable
A surprisingly common typo is WATCHTOWER_NOTIFICATION_LEVEL — the real variable is WATCHTOWER_NOTIFICATIONS_LEVEL (plural). Watchtower ignores unknown variables without complaint, so the misspelled version has simply never worked. Be aware when fixing it: error suppresses the routine update reports, which you may have been receiving (and enjoying) all along.
Migration Script
For a single host, editing by hand is quick. For several, this repository ships a tested Bash script that reads a compose file, builds the URL (including the percent-encoding), and rewrites the file:
curl -fsSLO https://wiki.b717.dev/assets/watchtower-notify-migrate.sh
chmod +x watchtower-notify-migrate.sh
./watchtower-notify-migrate.sh docker-compose.yml
Without options it is a dry run: it prints the generated URL and a diff, and writes nothing. -i rewrites the file in place and keeps a .bak backup; -o newfile.yml writes elsewhere. It handles both environment: styles (- KEY=value lists and KEY: value maps), preserves indentation, moves SUBJECTTAG and DELAY to their new variables, and passes compose references like ${SMTP_PASS} through unencoded so variable substitution keeps working. Read the script before running it — it is short, and that habit is worth keeping regardless of the source.
The fork also has a built-in converter: docker compose run --rm watchtower notify-upgrade reads the legacy variables from the environment and prints the equivalent URL. It does not rewrite your compose file, but it is a good cross-check for the script's output.
Verify
Syntax first:
Then apply:
The fork sends a startup notification by default, so a working SMTP configuration announces itself within a minute — no waiting for the next scheduled run. If the mail does not arrive, check the container logs for the SMTP handshake error:
Once everything works, the startup mail can be disabled with WATCHTOWER_DISABLE_STARTUP_MESSAGE=true.