Make Your Linux Server Send Website Alerts to Telegram

A website error triggers a notification on a glass smartphone as a mint penguin notices the alert. Linux & Administration
Let your Linux server send website alerts to Telegram. Set up scheduled checks, avoid repeated notifications, and test both failure and recovery messages.
Linux Website Monitoring · Lesson 02 / 03

Your website check should reach you.

A failed check in a terminal is easy to miss. A message on your phone is harder to overlook. Let Linux check a page on a schedule, tell you when the check fails, and send another message when it passes again.

We will build a small monitor with a private Telegram bot. It remembers the last state it successfully reported, so an unchanged failure does not produce a message every five minutes.

A website check becomes a notification when its state changes A browser page connects to a small state record, then to a phone. The state record prevents repeated unchanged results from creating repeated alerts. State changed? Notify once CHECK FAILED RECOVERED
The schedule runs the check. The saved state decides whether a new notification is needed. Failure and recovery are separate events.
BEFORE YOU START

A small monitor with a specific job

Use a Linux machine with Python 3.8 or newer, Bash and a working user crontab. Python’s standard library handles HTTPS, JSON and local locking; no pip packages are needed. You also need a Telegram account and outbound access to your website and Telegram’s API.

Choose a small public page you own. The rule is the same as in Lesson 01: expect HTTP 200 and a stable piece of text in the returned HTML. This check does not execute JavaScript.

01 / CREATE YOUR BOT

Give the monitor its own sender

  1. Open Telegram’s official @BotFather. Check the exact username.
  2. Send /newbot and follow the naming instructions.
  3. Keep the API token BotFather gives you. Treat it as a password.
  4. Open your new bot’s chat, tap Start, and send monitor setup.

Use a fresh bot for this exercise. A bot already connected to another application may have a webhook or another update reader, which can interfere with the next step.

02 / FIND YOUR PRIVATE CHAT

Find where the message should go

Run this in your terminal after sending your bot the setup message. It asks for the token without displaying it and prints the numeric IDs of private chats found in the bot’s available updates. It does not print message contents.

BASH · LOOK UP YOUR PRIVATE CHAT ID
python3 - <<'PY'
import getpass
import json
from urllib.request import urlopen

token = getpass.getpass('Bot token: ')
try:
    with urlopen(
        'https://api.telegram.org/bot' + token + '/getUpdates',
        timeout=15
    ) as response:
        data = json.load(response)
    if data.get('ok') is not True:
        raise ValueError()
    ids = {
        item['message']['chat']['id']
        for item in data.get('result', [])
        if item.get('message', {}).get('chat', {}).get('type') == 'private'
    }
    if not ids:
        print('No private chat found. Send the bot a new message and retry.')
    for chat_id in sorted(ids):
        print('Private chat ID:', chat_id)
except Exception:
    print('Lookup failed. Check the token, connection and bot setup.')
PY

For a fresh bot used only by you, expect your own chat ID. If several appear, identify your intended private chat before continuing. Do not guess the recipient.

Why must I message the bot first?

The setup message creates an update that includes your private chat ID. Starting the conversation also lets the bot send messages back to that chat.

03 / SAVE PRIVATE SETTINGS

Keep the token outside the program

The next block creates a private settings file under your home directory. Enter your real HTTPS URL, a short marker from its HTML, your token and your chat ID. Use a public URL without passwords or sensitive query parameters: the URL will appear in Telegram notifications.

BASH · CREATE SETTINGS ONCE
python3 - <<'PY'
import getpass
import json
import os
from pathlib import Path

os.umask(0o077)
base = Path.home() / '.local/share/sa-website-monitor'
base.mkdir(parents=True, exist_ok=True)
base.chmod(0o700)
settings = base / 'settings.json'
if settings.exists():
    raise SystemExit('Settings already exist. Edit that file instead.')

config = {
    'url': input('Public HTTPS page URL: ').strip(),
    'expected_text': input('Expected text in its HTML: '),
    'bot_token': getpass.getpass('Bot token: ').strip(),
    'chat_id': input('Private chat ID: ').strip()
}
with settings.open('x', encoding='utf-8') as handle:
    json.dump(config, handle, indent=2)
print('Private settings saved.')
PY

The directory is restricted to your account, and newly created files use private permissions. The token is stored as plain text in that protected file; this is access control, not encryption. Root and someone controlling your account can still read it.

What if I need to correct a setting?

Open ~/.local/share/sa-website-monitor/settings.json in your local editor. Keep valid JSON, preserve its private permissions, and avoid sharing the file. Do not rerun the setup to overwrite it.

04 / ADD THE MONITOR

Remember what you last reported

Create the script file:

BASH · OPEN THE FILE
nano "$HOME/.local/share/sa-website-monitor/monitor.py"

Paste the complete program below. In nano, save with Ctrl + O, press Enter, then exit with Ctrl + X. Use another editor if nano is unavailable.

PYTHON · MONITOR.PY
#!/usr/bin/env python3
import fcntl
import json
import os
from pathlib import Path
import re
import sys
from datetime import datetime, timezone
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import (
    HTTPRedirectHandler, Request, build_opener, urlopen
)

BASE = Path.home() / '.local/share/sa-website-monitor'


def log(message):
    stamp = datetime.now(timezone.utc).isoformat(timespec='seconds')
    print(f'{stamp} {message}', flush=True)


class HTTPSOnly(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        if urlsplit(newurl).scheme != 'https':
            raise URLError('HTTPS redirect required')
        return super().redirect_request(
            req, fp, code, msg, headers, newurl
        )


def check_page(config):
    try:
        request = Request(
            config['url'], headers={'User-Agent': 'SA-Website-Monitor/1.0'}
        )
        with build_opener(HTTPSOnly()).open(request, timeout=15) as response:
            if response.status != 200:
                return 'DOWN', f'HTTP {response.status}'
            body = response.read(1024 * 1024 + 1)
        if len(body) > 1024 * 1024:
            return 'DOWN', 'response exceeded the 1 MiB check limit'
        if config['expected_text'].encode('utf-8') not in body:
            return 'DOWN', 'expected text missing'
        return 'UP', 'HTTP 200 and expected text received'
    except HTTPError as error:
        return 'DOWN', f'HTTP {error.code}'
    except Exception:
        return 'DOWN', 'request failed: check network, TLS or timeout'


def send_message(config, message):
    payload = json.dumps({
        'chat_id': config['chat_id'], 'text': message
    }).encode('utf-8')
    request = Request(
        'https://api.telegram.org/bot' + config['bot_token'] + '/sendMessage',
        data=payload, headers={'Content-Type': 'application/json'}
    )
    try:
        with urlopen(request, timeout=15) as response:
            result = json.loads(response.read(65536))
            return response.status == 200 and result.get('ok') is True
    except Exception:
        # Never print the request URL: it contains the bot token.
        return False


def main():
    os.umask(0o077)
    if sys.argv[1:] not in ([], ['--test-alert']):
        raise ValueError('Use no arguments or --test-alert.')
    settings = BASE / 'settings.json'
    permissions = settings.stat()
    if permissions.st_uid != os.getuid() or permissions.st_mode & 0o077:
        raise ValueError('settings.json must be owned by you and private.')
    config = json.loads(settings.read_text(encoding='utf-8'))
    if not all(isinstance(config.get(k), str) and config[k] for k in
               ('url', 'expected_text', 'bot_token', 'chat_id')):
        raise ValueError('Four non-empty settings are required.')
    address = urlsplit(config['url'])
    if address.scheme != 'https' or not address.hostname or address.username:
        raise ValueError('Use a public HTTPS URL without embedded credentials.')
    if not re.fullmatch(r'[0-9]+:[A-Za-z0-9_-]+', config['bot_token']):
        raise ValueError('Check the bot token format.')
    if not re.fullmatch(r'[0-9]+', config['chat_id']):
        raise ValueError('Use the numeric ID of your private bot chat.')

    with (BASE / 'monitor.lock').open('a') as lock:
        try:
            fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            log('SKIPPED: another run is still active.')
            return 0

        if sys.argv[1:] == ['--test-alert']:
            if not send_message(config, 'TEST: your website alert channel works.'):
                log('TEST FAILED: Telegram delivery was not confirmed.')
                return 1
            log('TEST SENT: check your Telegram chat.')
            return 0

        state, reason = check_page(config)
        state_file = BASE / 'last-notified-state'
        previous = state_file.read_text().strip() if state_file.exists() else ''
        if previous not in ('', 'UP', 'DOWN'):
            raise ValueError('Invalid saved state; inspect last-notified-state.')
        log(f'{state}: {reason}')
        if state == previous:
            return 0

        label = 'INITIAL ' + state if not previous else (
            'RECOVERED' if state == 'UP' else 'CHECK FAILED'
        )
        message = f'{label}\n{config["url"]}\n{reason}'
        if not send_message(config, message):
            log('DELIVERY FAILED: saved notification state was not changed.')
            return 1

        pending = BASE / 'state.new'
        pending.write_text(state + '\n', encoding='utf-8')
        pending.replace(state_file)
        log('Notification accepted by Telegram; state saved.')
        return 0


if __name__ == '__main__':
    try:
        sys.exit(main())
    except Exception:
        log('MONITOR ERROR: check settings, permissions and local storage.')
        sys.exit(1)

The program keeps TLS verification enabled and rejects redirects to HTTP. It checks up to 1 MiB of response data, so choose a small page. Its 15-second network timeout is not a guaranteed total runtime limit; redirects, reads and name resolution can affect elapsed time.

When a notification is sent
Last reported state Current check Action
No saved state UP or DOWN Send the initial result.
UP DOWN Send CHECK FAILED.
DOWN DOWN Log the result; send no repeat.
DOWN UP Send RECOVERED.
Any state Delivery not confirmed Keep the previous reported state.
Can a notification still arrive twice?

Yes. Telegram might accept a message while its reply fails to reach the script. The script cannot confirm delivery and may retry later. This small monitor reduces repeated unchanged alerts; it does not guarantee exactly-once delivery or that you read the message.

05 / TEST DELIVERY

Make the phone receive a test message

BASH · TEST THE TELEGRAM CHANNEL
python3 "$HOME/.local/share/sa-website-monitor/monitor.py" --test-alert

You should receive TEST: your website alert channel works. This mode does not check the website or change the saved website state. If it fails, correct delivery before scheduling anything.

Then run the real check once:

BASH · RUN AND REPEAT
python3 "$HOME/.local/share/sa-website-monitor/monitor.py"
python3 "$HOME/.local/share/sa-website-monitor/monitor.py"

The first successful delivery reports the initial state. If the page check has the same result on the second run, the terminal shows the result but Telegram receives no duplicate.

06 / RUN ON A SCHEDULE

Let cron repeat the check

First find the full Python path and create the private log file:

BASH · PREPARE CRON
command -v python3
(
  umask 077
  touch "$HOME/.local/share/sa-website-monitor/monitor.log"
  chmod 600 "$HOME/.local/share/sa-website-monitor/monitor.log"
)
crontab -e

Add this line to your own crontab, preserving existing entries. Replace /usr/bin/python3 if the path printed above is different. Do not use sudo.

CRONTAB · EVERY FIVE MINUTES
*/5 * * * * /usr/bin/python3 "$HOME/.local/share/sa-website-monitor/monitor.py" >> "$HOME/.local/share/sa-website-monitor/monitor.log" 2>&1

The program locks its local run file. If an earlier run is still active, a new one skips instead of sending competing notifications. A run that hangs still needs investigation; the lock does not repair it.

After the next scheduled time, inspect the log:

BASH · VERIFY THE SCHEDULE
tail -n 20 "$HOME/.local/share/sa-website-monitor/monitor.log"

A fresh timestamp is evidence that the job ran. No new Telegram message can be normal when the result is unchanged. The log grows over time; include it in your retention or rotation plan.

For scheduling help, see running a script with cron. For the locking idea, read why overlapping jobs need protection.

07 / TEST FAILURE AND RECOVERY

Prove both messages can arrive

Pause this job by commenting out only its crontab line with #. Then edit the settings file and replace the expected text with SA_TEST_MARKER_NOT_PRESENT_7F3A. This changes your expectation, not the website.

  1. Run the monitor manually. If the saved state was UP and that marker is absent, expect CHECK FAILED.
  2. Run it again without changing anything. Expect no repeated failure notification.
  3. Restore the correct marker and run it once more. If the page passes, expect RECOVERED.
  4. Remove the comment from the cron line to resume checks.

A five-minute schedule can miss a failure that begins and ends between runs. This version also alerts on a single failed check, so a brief network problem may trigger a message. The wording deliberately reports the check result rather than declaring a universal outage.

Nothing arrives. What should I inspect?

Check that you started the bot, entered the correct private chat ID and token, and can reach Telegram. Read the local log for configuration or delivery errors. Do not share your settings file or token while troubleshooting.

Before you continue

  1. Why keep the bot token outside the script?
  2. Why does the monitor remember the last reported state?
  3. What happens when Telegram delivery is not confirmed?
  4. What does the test-alert mode leave unchanged?
  5. Can this monitor report its own machine losing power?
Check your answers
  1. It keeps credentials out of shared program code. The separate file still needs protection.
  2. So repeated unchanged results do not trigger repeated notifications after a confirmed delivery.
  3. The old reported state remains, allowing another attempt if a later check still differs.
  4. It does not change the saved website state or test the page.
  5. No. A stopped machine cannot run this script or send the message.
Rate article
Servers Academy
Add a comment