2025-07-27 12:54:30 +02:00
|
|
|
- name: Deploy ntfy and configure Caddy reverse proxy
|
ansible: add site.yml, and rename the monitoring group off the host's name
site.yml is a TABLE OF CONTENTS, not a second source of truth. It is 25
import_playbook: lines and comments - no `hosts:`, no `roles:`. Which hosts get
what stays on the `hosts:` line inside each playbook, exactly where it already
was; nothing moved. Every role is already wrapped in a thin playbook carrying
its own `hosts:` line, so there is no roles-vs-playbooks split to reconcile:
from here everything is a playbook.
What it buys:
What runs on a host? ansible-playbook site.yml --limit <host> --list-hosts
Who gets thing Y? the `hosts:` line in Y's own playbook
What is a host? ansible-inventory --graph
Note --list-hosts, not --list-tasks: the latter prints every play regardless of
--limit, so it will happily show you the bitcoin play under memos-box.
Nine playbooks are deliberately excluded and the file names every one with a
reason, so it accounts for all of them: the three infra/4xx monitoring plays
(still assert on the removed Uptime Kuma credentials and fail immediately),
910_docker (says `hosts: managed`, but Docker is on 5 of 11 managed hosts and
those 5 are exactly the ones that need it - running it installs Docker on the
Bitcoin node and the hypervisor), two nodito one-shots, the Kuma notification
setup, and two deliberate manual actions.
Writing it surfaced an inventory collision. There is a HOST named `monitoring`
in [vps] AND a group [monitoring], so Ansible warned and resolved `hosts:
monitoring` to the host:
[WARNING]: Found both group and host with same name: monitoring
The group is renamed to [observability]; the host keeps its name. [caddy:children]
and the two ntfy playbooks follow. Behaviour is unchanged - `hosts: monitoring`
already resolved to the host - but the ambiguity is gone and the warning with it.
Verified: inventory graph is warning-free, site.yml passes --syntax-check, and
per-host play counts are identical before and after the rename.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 21:24:09 +02:00
|
|
|
hosts: observability
|
2025-07-27 12:54:30 +02:00
|
|
|
become: yes
|
|
|
|
|
vars_files:
|
|
|
|
|
- ./ntfy_vars.yml
|
|
|
|
|
vars:
|
2025-11-06 23:09:44 +01:00
|
|
|
ntfy_subdomain: "{{ subdomains.ntfy }}"
|
2025-07-27 12:54:30 +02:00
|
|
|
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: |
|
2025-12-01 11:16:47 +01:00
|
|
|
(echo "{{ ntfy_password }}"; echo "{{ ntfy_password }}") | ntfy user add --role=admin "{{ ntfy_username }}"
|
2025-07-27 12:54:30 +02:00
|
|
|
|
ntfy, datum-gateway, headscale: use the caddy_site role
Completes Stage 3. No hand-rolled Caddy plumbing remains anywhere:
`grep sites-enabled` outside roles/ returns nothing, and so does
`grep "systemctl reload caddy"`.
ntfy uses caddy_site_body for its plain-HTTP listener and @httpget redirect.
Verified ok/unchanged against watchtower; the one other changed task is a
pre-existing "Update APT cache".
datum-gateway keeps a whole-Caddyfile validate after the role call. The role
validates its own fragment, but only a whole-file validate catches a conflict
between two sites, and this playbook was the only one that ever had it. Its
two debug tasks that echoed command output are gone with the commands.
headscale is the one that mattered. Its playbook wrote
`reverse_proxy localhost:8080`, but spacey is actually running a /admin*
route in front of Headplane behind Caddy basic auth. Running that playbook
would have deleted the admin route and its auth - a hazard that predates this
work. It now renders the config that is really there, verified ok/unchanged
via --start-at-task (the play cannot reach Caddy in check mode: "Install
headscale package" fails because the .deb is not really downloaded, before
and after this edit alike).
Supporting changes for headscale:
- headscale_ui_password_hash added to infra_secrets.yml and the identical
group_vars/all/vault.yml, read from the live config on spacey. The vault
already had headscale_ui_username (= counterweight, confirmed) and
headscale_ui_password; I did not verify the password is the plaintext of
this hash.
- headplane_port added to headscale_vars.yml.
- The role's handler now sets become: true. Handlers do not inherit become
from the task that notified them, and this play runs become: no.
- The include uses `apply: become: yes`; `become:` on an include_role is
rejected outright.
All 14 site files on all 3 hosts still byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 23:37:38 +02:00
|
|
|
- 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 }}
|
2025-07-27 12:54:30 +02:00
|
|
|
|
ntfy, datum-gateway, headscale: use the caddy_site role
Completes Stage 3. No hand-rolled Caddy plumbing remains anywhere:
`grep sites-enabled` outside roles/ returns nothing, and so does
`grep "systemctl reload caddy"`.
ntfy uses caddy_site_body for its plain-HTTP listener and @httpget redirect.
Verified ok/unchanged against watchtower; the one other changed task is a
pre-existing "Update APT cache".
datum-gateway keeps a whole-Caddyfile validate after the role call. The role
validates its own fragment, but only a whole-file validate catches a conflict
between two sites, and this playbook was the only one that ever had it. Its
two debug tasks that echoed command output are gone with the commands.
headscale is the one that mattered. Its playbook wrote
`reverse_proxy localhost:8080`, but spacey is actually running a /admin*
route in front of Headplane behind Caddy basic auth. Running that playbook
would have deleted the admin route and its auth - a hazard that predates this
work. It now renders the config that is really there, verified ok/unchanged
via --start-at-task (the play cannot reach Caddy in check mode: "Install
headscale package" fails because the .deb is not really downloaded, before
and after this edit alike).
Supporting changes for headscale:
- headscale_ui_password_hash added to infra_secrets.yml and the identical
group_vars/all/vault.yml, read from the live config on spacey. The vault
already had headscale_ui_username (= counterweight, confirmed) and
headscale_ui_password; I did not verify the password is the plaintext of
this hash.
- headplane_port added to headscale_vars.yml.
- The role's handler now sets become: true. Handlers do not inherit become
from the task that notified them, and this play runs become: no.
- The include uses `apply: become: yes`; `become:` on an include_role is
rejected outright.
All 14 site files on all 3 hosts still byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 23:37:38 +02:00
|
|
|
@httpget {
|
|
|
|
|
protocol http
|
|
|
|
|
method GET
|
|
|
|
|
path_regexp ^/([-_a-z0-9]{0,64}$|docs/|static/)
|
2025-07-27 12:54:30 +02:00
|
|
|
}
|
ntfy, datum-gateway, headscale: use the caddy_site role
Completes Stage 3. No hand-rolled Caddy plumbing remains anywhere:
`grep sites-enabled` outside roles/ returns nothing, and so does
`grep "systemctl reload caddy"`.
ntfy uses caddy_site_body for its plain-HTTP listener and @httpget redirect.
Verified ok/unchanged against watchtower; the one other changed task is a
pre-existing "Update APT cache".
datum-gateway keeps a whole-Caddyfile validate after the role call. The role
validates its own fragment, but only a whole-file validate catches a conflict
between two sites, and this playbook was the only one that ever had it. Its
two debug tasks that echoed command output are gone with the commands.
headscale is the one that mattered. Its playbook wrote
`reverse_proxy localhost:8080`, but spacey is actually running a /admin*
route in front of Headplane behind Caddy basic auth. Running that playbook
would have deleted the admin route and its auth - a hazard that predates this
work. It now renders the config that is really there, verified ok/unchanged
via --start-at-task (the play cannot reach Caddy in check mode: "Install
headscale package" fails because the .deb is not really downloaded, before
and after this edit alike).
Supporting changes for headscale:
- headscale_ui_password_hash added to infra_secrets.yml and the identical
group_vars/all/vault.yml, read from the live config on spacey. The vault
already had headscale_ui_username (= counterweight, confirmed) and
headscale_ui_password; I did not verify the password is the plaintext of
this hash.
- headplane_port added to headscale_vars.yml.
- The role's handler now sets become: true. Handlers do not inherit become
from the task that notified them, and this play runs become: no.
- The include uses `apply: become: yes`; `become:` on an include_role is
rejected outright.
All 14 site files on all 3 hosts still byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 23:37:38 +02:00
|
|
|
redir @httpget https://{host}{uri}
|
2025-07-27 12:54:30 +02:00
|
|
|
|
|
|
|
|
handlers:
|
|
|
|
|
- name: Restart ntfy
|
|
|
|
|
systemd:
|
|
|
|
|
name: ntfy
|
|
|
|
|
state: restarted
|