Skip to content

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:

smtp://user:password@host:port/?fromaddress=...&toaddresses=...&encryption=...&usestarttls=...
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:

watchtower-notify-migrate.sh

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:

docker compose config -q

Then apply:

docker compose up -d

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:

docker compose logs watchtower

Once everything works, the startup mail can be disabled with WATCHTOWER_DISABLE_STARTUP_MESSAGE=true.