Install and Use ntfy

Introduction

In my previous post, I used QQ Mail as the notification service for ChangeDetection. That was just for quickly getting started with ChangeDetection, but it wasn’t necessary since there are several open-source notification systems available, such as ntfy. This article will walk through the installation and usage of ntfy, which is quite straightforward. However, I encountered some issues when integrating it with ChangeDetection, and this post will cover the solutions to those problems as well.

Installation and Configuration

Docker compose

The ntfy Web UI uses the Notifications API, which requires HTTPS, so we need to launch an Nginx container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
version: "3"

services:
nginx:
image: nginx:1.24
container_name: nginx-ntfy
volumes:
- /opt/docker/nginx_conf/nginx-ntfy.conf:/etc/nginx/nginx.conf:ro
- /opt/docker/nginx_ssl:/etc/nginx/ssl/:ro
restart: unless-stopped
ports:
- 40043:443
depends_on:
- ntfy

ntfy:
image: binwiederhier/ntfy:v2.11.0
container_name: ntfy
command:
- serve
environment:
- TZ=Asia/Shanghai
volumes:
- ntfy_data:/var/cache/ntfy
- ntfy_config:/etc/ntfy
# ports:
# - 50080:80
healthcheck: # optional: remember to adapt the host:port to your environment
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

volumes:
ntfy_data:
external: true
ntfy_config:
external: true

ntfy Configuration

Manually create the configuration file: /etc/ntfy/server.yml. For specific configuration options, refer to the template provided by the official site. My configuration includes the following options:

1
2
3
4
5
6
base-url: "https://<host_IP>:40043"
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "24h"
auth-file: "/var/cache/ntfy/user.db"
auth-default-access: "deny-all"
behind-proxy: true

By default, ntfy doesn’t persist messages, only forwarding them in real-time. This can lead to message loss in two scenarios:

  • ntfy is online, but the client experiences a network issue. Upon reconnection, the client won’t receive messages sent during the outage.
  • The client is online, but ntfy goes offline before forwarding a message. Upon restarting, the message is lost.

Using the cache-file essentially turns ntfy into a message queue, mitigating these issues. The duration for which messages are cached is set by cache-duration.


ntfy also supports sending images and attachments, but since I don’t need those features, I didn’t configure them.

Nginx Configuration

For Nginx and SSL certificate setup, refer to my previous post.

The official document provides an Nginx template. Here’s my configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;

keepalive_timeout 65;

server {

listen 443 default_server ssl http2;
listen [::]:443 ssl http2;

ssl_certificate /etc/nginx/ssl/nginx_server.crt;
ssl_certificate_key /etc/nginx/ssl/nginx_server.key;

location / {

proxy_pass http://ntfy:80;
proxy_http_version 1.1;

proxy_buffering off;
proxy_request_buffering off;
proxy_redirect off;

proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_connect_timeout 3m;
proxy_send_timeout 3m;
proxy_read_timeout 3m;

client_max_body_size 0; # Stream request body to backend
}
}
}

VM NAT and Host Firewall

My host machine is running Win10, with a Linux VM running Docker via VMware. Therefore, I have to configure VM NAT and open up the host’s firewall. You can refer to this post for more details.

I opened port 40043 on the host to enable local network sharing.

Usage

For a basic Hello World example, see the official site. I won’t go into it here.

Access Control

You might have noticed that my configuration file includes:

1
auth-default-access: "deny-all"

By default, clients connected to ntfy have no read or write permissions for any topics. This is the most secure setup, but it’s also the most cumbersome to manage.

To add a test topic and allow everyone to read/write:

1
2
3
4
5
6
/ # ntfy access everyone test rw
granted read-write access to topic test

user * (role: anonymous, tier: none)
- read-write access to topic test
- no access to any (other) topics (server config

Integration with ChangeDetection

Access Control

For the ChangeDetection topic, I created a writer and a reader:

1
2
3
4
5
/ # ntfy access
user ChangeDetectionPublisher (role: user, tier: none)
- write-only access to topic ChangeDetection
user homeDevice (role: user, tier: none)
- read-only access to topic ChangeDetection

Remembering these users’ passwords can be tricky. I use a randomly generated long password for the writer, making this user essentially one-time use for a single topic. If I forget the password, I’ll just create a new user. For the reader, I use a short, easy-to-remember password (with some risk of being cracked).

Notification URL

According to the official documentation, the notification URL for ChangeDetection is:

1
ntfys://ChangeDetectionPublisher:<password>@<VM_IP>:40043/ChangeDetection
  • Since I’m using HTTPS, the prefix is ntfys; if using HTTP, it would be ntfy.
  • Regardless of which Docker network the container is in, it will correctly resolve the host’s IP (which in this case is the VM’s IP), no matter which network the IP belongs to (e.g., Docker network, VMware adapter network).

Container CA Certificate Configuration

After configuring the ChangeDetection notification URL and clicking test, the following error appeared:

1
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')))

This error is expected because Nginx is using a self-signed SSL certificate, and the ChangeDetection container doesn’t trust my self-created CA certificate.

Solution 1: Trust the Certificate in the Container

First, copy the self-signed certificate into the container:

1
docker cp /path/to/your/certificate.crt <container_name>:/usr/local/share/ca-certificates/

Then, inside the container, update the CA certificates:

1
update-ca-certificates

Finally, restart the container.

This solution works for curl, wget, and similar commands, but it doesn’t work for ChangeDetection because it’s developed in Python. Python doesn’t use the OS’s certificates but instead uses the certifi package’s certificates. So, the focus should be on making Python trust the certificate rather than the container.

Solution 2: Use the REQUESTS_CA_BUNDLE Environment Variable

Modify the ChangeDetection Docker Compose file:

1
2
3
4
5
6
7
services:
changedetection:
image: dgtlmoon/changedetection.io
environment:
- REQUESTS_CA_BUNDLE=/path/in/container/your_certificate.crt
volumes:
- /path/to/certificate.crt:/path/in/container/your_certificate.crt

This method is also not correct for ChangeDetection, as setting this environment variable overrides the entire set of existing certificates, meaning ChangeDetection will only trust the ntfy certificate and won’t trust any certificates on the internet. Since ChangeDetection needs to access the internet, this creates a conflict.

For some scripts, like the one I previously wrote about “Display Original Filenames in Jellyfin”, using this environment variable can still be quite handy.

Solution 3: Add the Certificate to the certifi Package

Enter the container:

1
2
3
4
5
6
python
>>> import certifi
>>> certifi.where()
/path/to/the/certifi/cacert.pem
>>> exit()
cat /path/in/container/your_certificate.crt >> /path/to/the/certifi/cacert.pem

Then restart the container.

This is the only viable solution at the moment.

For containers, it’s okay if the certifi package gets messed up, because you can always create a new container. For hosts, consider creating a new venv or conda env for this task, so you don’t damage your existing environment.


After testing ChangeDetection again, everything is working fine. The integration is completed 😁.

References