(Update) I added the use of access tokens.
Hi there,
today I have something pretty cool.
NTFY. An HTTP-based notification server, that also has a nice phone app. It allows me to get notification from basically anything I want. To tell you the truth, I don’t have to may use cases so far, except for failed backups. But, you can send notifications with Ansible (though it took me a while to get it running with authentication, even though it’s very simple), which makes the possibilities basically endless.
I will also show the deployment and configuration of Nginx-Proxy-Manager so that we can easily secure our NTFY server with a certificate.
Let’s begin.
I will be using docker for the deployment, since it makes this a breeze and the base OS doesn’t really matter. For the OS, I will be using Rocky Linux 8.9.
Deploying the docker containers
Docker installation
We will start with the installation of docker.
First, start with adding the docker repo.
# Adding the repository docker :: ~ » sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Next, install the required packages.
# Installing the docker packages docker :: ~ » sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Last step for this. Enabling and starting the service.
# Enable the service and start it docker :: ~ » sudo systemctl --now enable docker
Alright. Let’s continue with the ntfy server deployment.
Deploying NTFY
Create a subfolder for the docker-compose file. I will use the home folder for this. In here, we will create a “docker-compose.yml” file with the following content. The content is from the main documentation with just a couple of changes.
Take note of the changed port marked in red. You can choose here whatever port you want, we will need this later for the nginx-proxy-server. If you don’t want to use nginx for the reverse proxy, just leave it at 80. I also highlighted the paths to the NTFY configuration, cache and lib files in green. We also need to create those.
# Create the folder docker :: ~ » mkdir ntfy # Navigate into that folder docker :: ~ » cd ntfy # Create a file named docker-compose.yml docker :: ntfy » vim docker-compose.yml version: "2.3" services: ntfy: image: binwiederhier/ntfy container_name: ntfy command: - serve environment: - TZ=Europe/Berlin volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy - /var/lib/ntfy:/var/lib/ntfy ports: - 0.0.0.0:8880:80 healthcheck: 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
Create the folders.
Create the folders. "-p" will create the parent folder if it doesn't exist docker :: ntfy » sudo mkdir /var/cache/ntfy -p docker :: ntfy » sudo mkdir /etc/ntfy docker :: ntfy » sudo mkdir /var/lib/ntfy -p
We also need a “server.yml” in the “/etc/ntfy” folder with a few settings we should set. I highlighted the parts that you should adapt to your environment. The paths are the same as in the docker-compose file above, marked in green.
If you don’t need authentication, remove the lines that start with “auth”.
docker :: ntfy » sudo vim /etc/ntfy/server.yml base-url: "<your-fqdn-url>" upstream-base-url: "https://ntfy.sh" listen-http: ":80" cache-file: "/var/cache/ntfy/cache.db" attachment-cache-dir: "/var/cache/ntfy/attachments" behind-proxy: true # This creates a secondary database used to manage your users and permissions auth-file: /var/lib/ntfy/auth.db auth-default-access: "deny-all" auth-startup-queries: pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory; pragma busy_timeout = 15000; vacuum; # Rate limiting in case a message source goes a bit nuts. visitor-subscription-limit: 10 visitor-attachment-total-size-limit: "50M" visitor-attachment-daily-bandwidth-limit: "100M"
Ok. Now we can deploy the container. You have to be in the folder with the “docker-compose.yml” file.
# Deploy the docker container docker :: ntfy » sudo docker compose up -d
At this point, the container should be up and running. Check the status of the container with the “docker ps” command.
# Check container status docker :: ntfy » sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b1d4e03a2278 binwiederhier/ntfy "ntfy serve" 4 minutes ago Up 4 minutes (healthy) 0.0.0.0:8880->80/tcp ntfy
Alright. One more thing before we can check the WebUI. We have to set up the firewall to allow the port 8880/TCP.
# Add the firewall rule docker :: ntfy » sudo firewall-cmd --add-port=8880/tcp docker :: ntfy » sudo firewall-cmd --add-port=8880/tcp --permanent
Ok. Now we should be able to access the UI. Open a browser and type in the IP of the server with the port at the end. For example http://10.10.0.200:8880 and you should see something like this.
At this point normally we could publish notifications, add new topics and so on. But we are not done yet (and we set the default to deny-all). Primarily because I want to have authentication and SSL. To actually receive notifications with your phone, you need an SSL certificate anyway. So let’s take care of that first.
For this, I will be deploying the Nginx-Proxy-Manager. You could also just use a nginx server to provide the reverse proxy, but I am lazy and also want to showcase the proxy-manager. Let’s continue.
Deploying Nginx-Proxy-Manager
We will start the same way. Create a subfolder and a “docker-compose.yml” file with the following content.
# Navigate back to the home folder docker :: ntfy » cd ~ # Create a subfolder for the nginx-proxy-manager docker :: ~ » mkdir nginx-proxy-manager # Navigate into that folder docker :: ~ » cd nginx-proxy-manager # Create the file docker :: nginx-proxy-manager » vim docker-compose.yml version: '3.8' services: app: image: 'jc21/nginx-proxy-manager:latest' restart: unless-stopped ports: # These ports are in format <host-port>:<container-port> - '80:80' # Public HTTP Port - '443:443' # Public HTTPS Port - '81:81' # Admin Web Port # Add any other Stream port you want to expose # - '21:21' # FTP # Uncomment the next line if you uncomment anything in the section # environment: # Uncomment this if you want to change the location of # the SQLite DB file within the container # DB_SQLITE_FILE: "/data/database.sqlite" # Uncomment this if IPv6 is not enabled on your host DISABLE_IPV6: 'true' volumes: - /etc/nginx-proxy-manager/data:/data - /etc/nginx-proxy-manager/letsencrypt:/etc/letsencrypt
I highlighted the paths in red which you might want to change.
Create the highlighted folders.
# Create the folders. "-p" will create the parent folder if it doesn't exist docker :: nginx-proxy-manager » sudo mkdir /etc/nginx-proxy-manager/data -p docker :: nginx-proxy-manager » sudo mkdir /etc/nginx-proxy-manager/letsencrypt -p
Ok. Now let’s configure the firewall.
# Add the firewall rules docker :: nginx-proxy-manager » sudo firewall-cmd --add-service http --add-service https --permanent docker :: nginx-proxy-manager » sudo firewall-cmd --add-service http --add-service https docker :: nginx-proxy-manager » sudo firewall-cmd --add-service 81/tcp --permanent docker :: nginx-proxy-manager » sudo firewall-cmd --add-service 81/tcp
Now, let’s start the container.
# Starting the docker container docker :: nginx-proxy-manager » sudo docker compose up -d
Alright. That’s it for the deployment. Next, we will configure the access control on the NTFY server, to make sure only your systems can create notifications. For this, we will have to connect to the NTFY container.
Server Configuration
NTFY Access Control (Update)
Username Password
Let’s connect to the container. First, get the container ID.
# List container name and ID docker :: ~ » sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b1d4e03a2278 binwiederhier/ntfy "ntfy serve" 10 minutes ago Up 10 minutes (healthy) 0.0.0.0:8880->80/tcp ntfy
Now that we have the ID we can connect to it. We don’t have to input the whole ID, as long as it’s unique. In my case, the first 2 characters are enough.
After that, we can create an “admin” user.
# Connect to the ntfy container docker :: ~ » sudo docker exec -it b1 /bin/sh # Create a admin user / # ntfy user add --role=admin admin password: confirm: user admin added with role admin
Next, I want an “ansible” user. This is for my ansible roles, I will show this later.
# Create a normal user / # ntfy user add ansible user ansible added with role user
Now we create a topic “Ansible-Notifications” in my case and the read-write permissions for the “ansible” user.
# Create a topic and grant permissions / # ntfy access ansible Ansible-Notifications rw
Access Token
If you want to use access tokens, use the following command. If you don’t want it to expire, just remove the “–expires=120d” option.
# Create an access token for the "ansible" user / # ntfy token add -–expires=120d –-label=”TOKEN” ansible token tk_fqqq93msikzgdzae8sso5s32fnodg created for user ansible, expires Sun Feb 25 08:58:49 UTC 2030
That’s it for the permissions, for now. You can of course create more. I also created a backup user and topic.
Nginx-Proxy-Manager configuration
Now we will set up a Let’s Encrypt certificate using the Nginx-Proxy-Manager. For this, I will assume that you have the correct ports forwarded to your Nginx-Proxy-Server and have a domain registered.
Open a browser and type in the URL for your nginx-proxy-server. For example http://10.10.0.200:81
The default login credentials are.
username: admin@example.com
password: changeme
You will immediately be asked to change the credentials. After this, you should be on the dashboard.
You might notice, that I already have 2 reverse proxy configurations. Your setup should say “0 Proxy Hosts” Just ignore that in my screenshot.
First, we need a certificate. Navigate to “SSL Certificates” and click on “Add SSL Certificate”.
Set it up how you need it with your domain and click on “Save”. This should generate a new Let’s Encrypt certificate for you.
Once that’s done, navigate to “Hosts” -> “Proxy Hosts” and click on “Add proxy Host”
Define the hostname, make sure your DNS is configured correctly, type in the forward IP and the port we defined earlier for the ntfy server.
Under the SSL index, select your newly generated certificate and click on save.
That’s it. It should now look similar to this.
Now we should be able to browse via https to the ntfy page.
Open your browser and type in the URL you defined. Something like https://ntfy.example.de/. If you can reach it we can test the notification function. If not, check your DNS configuration and firewall setup.
Testing the Notification
Let’s test it. For this we will use curl. With “-u” you can set the “username”:”password” and with “-d” you define the message. The last part of the URL is the topic you want to send it to.
# Send a test notification to the Ansible Topic. fedora :: ~ » curl -u ansible:tk_gys85asdfa323cq -d TEST https://<fqdn>/Ansible {"id":"ZCom45xFtHMW","time":1701506910,"expires":1701550110,"event":"message","topic":"Ansible","message":"TEST"}
If you want to use the token, use the following command. It’s the same, just remove the username and replace the password with the access token.
fedora :: ~ » curl -u :tk_fqqq93msikzgdzae8sso5s32fnodg -d TEST https://<fqdn>/Ansible
Ok. Open your browser and navigate to the NTFY page. Since it’s our first login, we have to subscribe to a topic.
Type in the topic and your credentials.
And this is, what it should look like.
Alright. Let’s take a look at a Ansible task.
Ansible Task Notification (Update)
The next few file locations really depend on your setup of Ansible, but I will try to make it as clear as possible.
I have a role for my linux servers. This does things like to install updates, create folders and local users etc.
The structure looks like this. Folders are blue and files orange.
- ansible-role-all.yml
- ansible-role-all
- files
- tasks
- main.yml
- ntfy_notification.yml
- vars
- main.yml
There are a few more tasks, but those are not really relevant for this, so lets try to keep this simple.
Here is the content of the “vars” “main.yml” file.
--- # ntfy credentials: ntfy_user: ansible ntfy_password: <super secure password> ntfy_topic: Ansible ntfy_access_token: <your-access-token>
And here is the content of the “tasks” “main.yml” and “ntfy_notification.yml” file.
main.yml
- import_tasks: ntfy_notificaton.yml tags: ntfy
ntfy_notification.yml
- name: Send ntfy.example.de update uri: url: "https://ntfy.example.de/{{ ntfy_topic }}" method: POST headers: Title: "NTFY Title" body: "This is the text you want to send. You can also use ansible variables here like {{ inventory_hostname }}" password: "{{ ntfy_password }}" user: "{{ ntfy_user }}" force_basic_auth: true
Here is a version using an access token.
- name: Send ntfy.example.de update uri: url: "https://ntfy.example.de/{{ ntfy_topic }}" method: POST headers: Title: "NTFY Title" Authorization: "Bearer {{ ntfy_access_token }}" body: "This is the text you want to send. You can also use ansible variables here like {{ inventory_hostname }}" force_basic_auth: true
So, what does it look like. Let’s run the ansible role.
# Running the ansible role fedora :: Ansible » ansible-playbook -i ansible-role-all.yml --tags ntfy PLAY [all:!localhost] ****************************************************************************************************************** TASK [Gathering Facts] ****************************************************************************************************************** ok: [nextcloud] TASK [ansible-role-all : Send ntfy.example.de update] ****************************************************************************************************** ok: [nextcloud] PLAY RECAP ****************************************************************************************************************** nextcloud : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
On the WebUI.
By the way. You should encrypt the ansible vars file, since it contains cleartext password. Here a quick guide on encrypting files in ansible.
Ansible File Encryption (Optional)
Use the ansible-vault command to encrypt the files.
fedora :: Ansible » ansible-vault encrypt ansible-role-all/vars/main.yml New Vault password: Confirm New Vault password: Encryption successful
If you want to edit the file, use the “edit” option. This will open the file in nano.
fedora :: Ansible » ansible-vault edit ansible-role-all/vars/main.yml
To unencrypt the file, use the “decrypt” option.
fedora :: Ansible » ansible-vault decrypt ansible-role-all/vars/main.yml
Ok. That’s it. I will try to make more use of this whole ansible in combination with ntfy stuff. A use case, for me, would be for longer tasks to inform me when they are done. But I have to experiment with it. Anyways. Hope you found this interesting.
If you want to know how to update your Docker containers with Ansible. Here is an older post, with the tasks for that.
Till next time.
Hi! Do you know the parameter for using ntfy notifications with access tokens instead of username and password?
Hi,
you can create a token with:
ntfy token add –expires=120d –label=”TOKEN” username
This needs an existing user. Once that’s done, you can list the tokens with this command:
ntfy token list
To send a message with the token rather than the username and password use this command (note the “:” after the -u) :
curl -u :token-id -d “This is a token test message” “https://ntfy-url/Topic”
It’s basically the same command, just remove the usename and replace the password with the token.
Hope this helps.
Thanks for the answer, but I’m refering to using the acces token with the ansible module uri.
For example with the ntfy_notification.yml, that you explained with this post.
I tried with the headers parameter of the uri module using the base64 as the ntfy documentation point but, I keep getting this error: FAILED! => {“changed”: false, “msg”: “argument ‘headers’ is of type and we were unable to convert to dict: dictionary update sequence element #0 has length 1; 2 is required”}
Ah I misread your first post.
This is how it worked in my case.:
– name: Send Test Notification
uri:
url: “https://ntfy-url/Topic”
method: POST
headers:
Title: “Notification Title”
Authorization: “Bearer this-is-your-token”
body: “Filler text”
force_basic_auth: true
Remove the username and password and add “Authorization” below the “headers”.
I will update the post to make it more readable.
So it was an indentation problem. Thanks a lot! Is working perfectly now.
Glad it worked.