watchtower: remove from the estate, and with it ntfy

watchtower is being destroyed. Removed from [vps], with its host_vars, its push
token, and the six Gatus endpoints that referenced it (liveness, disk, two
systemd services, the ntfy DNS record and the ntfy HTTP check).

ntfy went with it - it ran nowhere else - so services/ntfy is deleted,
subdomains.ntfy and ntfy_topic are gone from group_vars, and the ntfy playbook
is out of site.yml. ntfy_topic already had no readers: the three infra/4xx plays
that used it were deleted when their checks were superseded.

Two things this exposed.

services/ntfy/deploy_ntfy_playbook.yml was pointing at the WRONG MACHINE. It
said `hosts: observability`, which resolves to the host `monitoring`
(64.226.70.190) - but ntfy ran on watchtower, and ntfy.contrapeso.xyz pointed
there. Running it would have installed ntfy on the new VPS. Moot now, but it is
the same stale-identity failure as the rest: the group meant watchtower when the
play was written, and nobody revisited it when the group changed. Watchtower was
in [vps] and NO role group at all, while running caddy, ntfy and Uptime Kuma -
nothing in the repo managed any of it.

More seriously: ntfy-emergency-app on vipy (avisame.contrapeso.xyz) sends its
notifications to https://ntfy.contrapeso.xyz, topic "emergencia". Destroying
watchtower breaks it, and it is an EMERGENCY notifier - it would fail silently
at exactly the moment it matters. That is NOT resolved here, deliberately:
standing ntfy up elsewhere, pointing at ntfy.sh, or retiring the app are all
decisions, not cleanups.

What this change does is make the break impossible to miss. The URL was derived
from subdomains.ntfy, so deleting that would have turned it into an undefined
variable buried in a template. It is now an explicit ntfy_service_url in the
app's own vars, still holding the old value, with the three options written
above it. The ntfy credentials stay in the vault because that app still needs
them - the vault was restored from HEAD and only watchtower's push token
removed, rather than re-handling the plaintext.

Verified: no reference to watchtower or its IP anywhere in the repo; Gatus down
from 91 to 85 endpoints, 85 UP, 0 DOWN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
counterweight 2026-09-14 11:26:47 +02:00
parent bf3d21fef7
commit 85040d5f67
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
10 changed files with 171 additions and 282 deletions

View file

@ -6,8 +6,17 @@
vars:
ntfy_emergency_app_subdomain: "{{ subdomains.ntfy_emergency_app }}"
ntfy_emergency_app_domain: "{{ ntfy_emergency_app_subdomain }}.{{ root_domain }}"
ntfy_service_domain: "{{ subdomains.ntfy }}.{{ root_domain }}"
ntfy_emergency_app_ntfy_url: "https://{{ ntfy_service_domain }}"
# ⚠ UNRESOLVED: this app sends its notifications to an ntfy server, and the
# server it points at ran on watchtower, which is being destroyed. This was
# derived from subdomains.ntfy, which is now gone with it.
#
# Until an ntfy server exists again this URL is dead, and the app fails
# silently at exactly the moment it matters - it is an EMERGENCY notifier.
# Three ways out, none of them automatic:
# * stand ntfy up somewhere else (the monitoring VPS, or vipy)
# * point this at the public ntfy.sh
# * retire the app
ntfy_emergency_app_ntfy_url: "{{ ntfy_service_url }}"
ntfy_emergency_app_ntfy_user: "{{ ntfy_username | default('') }}"
ntfy_emergency_app_ntfy_password: "{{ ntfy_password | default('') }}"

View file

@ -14,3 +14,9 @@ remote_host: "{{ hostvars.get(remote_host_name, {}).get('ansible_host', remote_h
remote_user: "{{ hostvars.get(remote_host_name, {}).get('ansible_user', 'counterweight') }}"
remote_key_file: "{{ hostvars.get(remote_host_name, {}).get('ansible_ssh_private_key_file', '') }}"
remote_port: "{{ hostvars.get(remote_host_name, {}).get('ansible_port', 22) }}"
# Where the emergency notifications are sent. This pointed at ntfy.contrapeso.xyz
# on watchtower; that host is being destroyed, so this MUST be repointed before
# the app can work again. Left at the old value so the break is visible rather
# than silently defaulted to something plausible.
ntfy_service_url: "https://ntfy.contrapeso.xyz"

View file

@ -1,97 +0,0 @@
- name: Deploy ntfy and configure Caddy reverse proxy
hosts: observability
become: yes
vars_files:
- ./ntfy_vars.yml
vars:
ntfy_subdomain: "{{ subdomains.ntfy }}"
ntfy_domain: "{{ ntfy_subdomain }}.{{ root_domain }}"
tasks:
- name: Ensure /etc/apt/keyrings exists
file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Download and dearmor ntfy GPG key
shell: curl -fsSL https://archive.heckel.io/apt/pubkey.txt | gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
args:
creates: /etc/apt/keyrings/archive.heckel.io.gpg
- name: Add ntfy APT repository
copy:
dest: /etc/apt/sources.list.d/archive.heckel.io.list
content: |
deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main
mode: '0644'
- name: Update APT cache
apt:
update_cache: yes
- name: Install ntfy
apt:
name: ntfy
state: present
- name: Ensure ntfy cache directories exist
file:
path: "{{ item }}"
state: directory
owner: ntfy
group: ntfy
mode: '0755'
loop:
- /var/cache/ntfy
- /var/cache/ntfy/attachments
- name: Deploy ntfy configuration file
copy:
dest: /etc/ntfy/server.yml
content: |
base-url: "http://{{ ntfy_domain }}"
listen-http: ":{{ ntfy_port }}"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
behind-proxy: true
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
owner: root
group: root
mode: '0644'
notify: Restart ntfy
- name: Enable and start ntfy service
systemd:
name: ntfy
enabled: yes
state: started
- name: Create ntfy admin user
shell: |
(echo "{{ ntfy_password }}"; echo "{{ ntfy_password }}") | ntfy user add --role=admin "{{ ntfy_username }}"
- name: Publish ntfy through Caddy
ansible.builtin.include_role:
name: caddy_site
vars:
caddy_site_name: ntfy
caddy_site_domain: "{{ ntfy_domain }}, http://{{ ntfy_domain }}"
# Raw body: ntfy needs a plain-HTTP listener for its CLI/app clients,
# with only GETs to the docs and topic paths redirected to HTTPS.
caddy_site_body: |
reverse_proxy 127.0.0.1:{{ ntfy_port }}
@httpget {
protocol http
method GET
path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/)
}
redir @httpget https://{host}{uri}
handlers:
- name: Restart ntfy
systemd:
name: ntfy
state: restarted

View file

@ -1,3 +0,0 @@
ntfy_port: 6674
# ntfy_topic lives in group_vars/all/main.yml