Send Discord Notifications from Linux

This guide covers setting up a Linux server to send notifications to a Discord channel using Discord’s incoming webhook API and a small Bash script. No third-party packages are required beyond curl, which is often installed by default on most Linux distributions.

Discord is a reasonable choice for low-frequency infrastructure notifications. It is free for small use cases, has reliable mobile and desktop apps, and requires no server-side configuration on your end. Its webhook delivery is not guaranteed to be instantaneous and can queue messages under load, so it is not suitable for latency-sensitive alerting, but it is adequate for “did this script run” and “is disk space getting low” use cases.

Prerequisites

This guide was tested on Ubuntu 22.04 and 24.04. The process applies to most systemd-based Linux distributions. You need root or sudo access, curl installed, outbound HTTPS access to discord.com, and a Discord account.

Verify curl is installed:

curl --version

If curl is not present, install it:

sudo apt install curl

Create a Discord Webhook

Go to discord.com and create an account if you do not have one. Enable two-factor authentication before doing anything else. Discord is a high-profile platform and account compromise is common.

Once logged in, create a new server using the plus icon in the left sidebar. A default #general text channel will be created automatically. Create a separate private text channel for alerting. Keeping it separate from general conversation means notifications do not get lost and you can easily mute or archive them independently.

Open the settings for that channel, navigate to Integrations, then Webhooks, and create a new webhook. Copy the webhook URL. It will look like this:

https://discord.com/api/webhooks/11525123456586252368/PaKsnnblahblahblahrhkuVNsdfgsdg

The numeric segment is the webhook ID and the trailing string is the token. Together they grant anyone who holds the URL the ability to post messages to that channel. Treat it as a password: do not commit it to a repository, do not share it, and regenerate it from the Discord settings if you believe it has been exposed.

Verify the Webhook Works

Before writing any scripts, confirm that your server can reach Discord and that the webhook URL is correct. Run this from the shell, substituting your actual webhook URL:

curl -H "Content-Type:application/json" \
  -d '{"username": "testhost", "content": "Posted via command line"}' \
  https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN

Discord’s webhook API returns HTTP 204 No Content on success and produces no response body. If curl exits without error and you see the message appear in the Discord channel within a few seconds, the connectivity and the URL are both correct.

If the message appears but with the webhook’s configured name rather than the username value you specified, that is expected behaviour in some cases. Discord allows the username field to override the display name for standard incoming webhooks, but this behaviour can be restricted by server-level settings or by the webhook type. Verify what your setup does and adjust your expectations accordingly. The message will still be delivered.

Store the Webhook URL in a Credentials File

Create a file at /etc/discord.conf to hold the webhook URL. Placing it in /etc keeps it separate from any individual user’s home directory and makes it accessible to root-owned cron jobs.

sudo nano /etc/discord.conf

Paste in one line:

webhook="https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN"

Save and close the file, then restrict its permissions:

sudo chmod 640 /etc/discord.conf
sudo chown root:root /etc/discord.conf

The 640 permission gives read-write to root and read to the owning group, with no access for others. This is more practical than 440 (read-only) because it allows root to edit the file without first changing permissions. If you have non-root users or service accounts that need to call the script, add them to a dedicated group and set that group as the file’s owning group:

sudo groupadd discord-alert
sudo usermod -aG discord-alert <YOUR_SERVICE_USER>
sudo chown root:discord-alert /etc/discord.conf
sudo chmod 640 /etc/discord.conf

Verify that an unprivileged user without the appropriate group membership cannot read the file:

cat /etc/discord.conf

Expected output: Permission denied.

Write the Alert Script

Create a directory for the script and open a new file:

sudo mkdir -p /opt/discord
sudo nano /opt/discord/discord.sh

Paste in the following:

#!/bin/bash
. /etc/discord.conf
curl -H "Content-Type:application/json" \
  -d "{\"username\": \"$1\", \"content\": \"$2\"}" \
  "$webhook"

The first line sets the interpreter. The dot (.) on the second line sources /etc/discord.conf, which loads the webhook variable into the current shell session. The curl call constructs a JSON body using the script’s first argument ($1) as the display name and the second argument ($2) as the message text. The $webhook variable is quoted to avoid word-splitting if the URL ever contains unusual characters.

The backslash-escaped quotes inside the double-quoted -d argument are necessary because the JSON keys and string values must be quoted, but the whole payload is already inside a double-quoted shell string. Single-quoting the payload does not work here because $1 and $2 would not be expanded inside single quotes.

Make the script executable:

sudo chmod +x /opt/discord/discord.sh

Test it:

sudo /opt/discord/discord.sh "server01" "Test message from script"

Check the Discord channel. The message should appear within a few seconds.

Set Up Cron Jobs

Open the root crontab:

sudo crontab -e

Add the following two lines:

@reboot sleep 20 && /opt/discord/discord.sh "server01" "Starting up..."
0 9 * * * /opt/discord/discord.sh "server01" "Disk space on / is $(df -h / | tail -n1 | awk '{print $4}') available"

The @reboot line runs once each time the system starts. The sleep 20 introduces a 20-second delay before the curl call, which gives the network stack and any DHCP lease time to come up. If the network is not available when the script runs, curl will fail silently. 20 seconds is a reasonable value for most systems, but if your server takes longer to establish a network connection after boot (for example, if it waits on a bonded interface or a DHCP server with a slow response), increase the value.

The 0 9 * * * schedule fires at 09:00 system time every day. The disk space command uses df -h / to get human-readable output for the root filesystem, pipes it to tail -n1 to get the data row (not the header), and uses awk ‘{print $4}’ to extract the fourth column, which is the available space figure. Note that this command expansion happens at the time the cron job fires, not when you save the crontab.

A note on the network dependency problem: cron’s @reboot has no awareness of systemd service dependencies. If your use case requires the message to fire only after a specific service is confirmed running, a systemd unit with After=network-online.target and Wants=network-online.target is a more reliable approach. For a simple “server is up” notification, the sleep workaround is adequate.

Save and close the crontab editor. Cron picks up the changes immediately without requiring a restart.

Testing and Validation

Webhook URL: Run the manual curl command from the verification step and confirm the message appears in Discord. An HTTP 204 response means the request was accepted. If you get HTTP 401 or 404, the URL is wrong or the webhook is at fault.

Credentials file permissions: As a non-root user who is not in the owning group, attempt to cat /etc/discord.conf. You should see “Permission denied”. As root, cat /etc/discord.conf should show the webhook line.

Script execution: Run sudo /opt/discord/discord.sh “testhost” “manual test” and confirm the message appears in Discord. If you get a permission error on the .conf file, check the group membership of the user running the script.

Reboot alert: Reboot the server and watch the Discord channel. Allow at least 30 seconds after the system comes back online before concluding the message was not sent. If the message does not arrive, log in and check the root crontab with sudo crontab -l to confirm the entry is present, then run the script manually with sudo /opt/discord/discord.sh “server01” “manual test” to verify connectivity.

Disk space cron job: Rather than waiting until 09:00, temporarily change the schedule to run two minutes from now (for example, if the time is 14:07, set it to 9 14 * * *), save the crontab, and wait. Check the Discord channel for the message. Then restore the intended schedule. Alternatively, run the df pipeline directly in your shell to confirm the column extraction is correct for your filesystem layout:

df -h / | tail -n1 | awk '{print $4}'

The output should be a single value such as 18G or 450M. If the output is blank or wrong, the column index may differ on your system. Run df -h / without the pipe to inspect the column layout and adjust the awk field number accordingly.

Extending the Script

Because discord.sh takes only two string arguments, it can be called from any script, cron job, or compatible third-party tool that supports running an external command.

Troubleshooting

curl: (6) Could not resolve host: discord.com – The server has no outbound DNS. Check /etc/resolv.conf and confirm you can ping 8.8.8.8 and curl https://discord.com separately.

HTTP 400 Bad Request – The JSON payload is malformed. Test with the static single-quoted version from the verification step first. If that works but the script does not, the issue is with quote escaping or a special character in the $1 or $2 values (apostrophes and double quotes in the message text will break the JSON). For production use where message content is unpredictable, pipe the payload through jq to construct valid JSON rather than using string interpolation.

HTTP 429 Too Many Requests – Discord rate-limits webhook posts. For standard webhooks the limit is 30 messages per minute per channel. This is unlikely to be a problem for the use cases in this guide.

Message appears but username is wrong – The username override is subject to Discord’s server settings and may not apply to all webhook configurations.

@reboot job does not fire – Confirm cron is running: systemctl status cron. On some distributions the service is named crond. Also confirm the entry is in the root crontab (sudo crontab -l) and not a user crontab.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

Your email address will not be published. Required fields are marked *

20 − eleven =