<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DevKit</title>
    <description>The latest articles on DEV Community by DevKit (@devkit_shop).</description>
    <link>https://dev.to/devkit_shop</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200465%2Ff571e3e5-a6bf-4853-92d5-e974eea7f015.png</url>
      <title>DEV Community: DevKit</title>
      <link>https://dev.to/devkit_shop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devkit_shop"/>
    <language>en</language>
    <item>
      <title>Monitor your VPS in 10 minutes: Prometheus, Grafana and Telegram alerts (free kit)</title>
      <dc:creator>DevKit</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:22:02 +0000</pubDate>
      <link>https://dev.to/devkit_shop/monitor-your-vps-in-10-minutes-prometheus-grafana-and-telegram-alerts-free-kit-37pc</link>
      <guid>https://dev.to/devkit_shop/monitor-your-vps-in-10-minutes-prometheus-grafana-and-telegram-alerts-free-kit-37pc</guid>
      <description>&lt;p&gt;Most people start thinking about monitoring after the first incident: the disk fills up with logs, the database dies from lack of memory, and you learn about it from your users. Basic protection can be set up in an evening, and faster if you don't assemble the stack by hand.&lt;/p&gt;

&lt;p&gt;Here is the minimal setup that is enough for one VPS. Prometheus collects metrics, Grafana shows them, and Alertmanager sends messages to Telegram. Everything runs with Docker Compose. I packaged the ready-made configs into a free kit, Monitoring Lite, and the commands below come from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;four containers: Prometheus, node-exporter, Alertmanager, Grafana;&lt;/li&gt;
&lt;li&gt;one Host Overview dashboard: CPU, memory, disk usage by mountpoint, network, load average, uptime;&lt;/li&gt;
&lt;li&gt;five alerts in Telegram: host down, high CPU, high memory, disk almost full, host rebooted;&lt;/li&gt;
&lt;li&gt;a Grafana password generated at install time, and ports that are closed to the outside world.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it fits together
&lt;/h2&gt;

&lt;p&gt;node-exporter reads the host's &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; and serves metrics over HTTP. Prometheus scrapes them every 15 seconds and stores them. Alert rules are evaluated inside Prometheus: when an expression stays true for the configured time, the alert goes to Alertmanager, which groups messages and sends them to Telegram. Grafana reads data from Prometheus.&lt;/p&gt;

&lt;p&gt;The datasource and dashboard are provisioned automatically, so you don't create anything by hand in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: a Telegram bot
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Message &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/botfather"&gt;@botfather&lt;/a&gt;&lt;/strong&gt; in Telegram, send &lt;code&gt;/newbot&lt;/code&gt;, and pick a name. You get a token like &lt;code&gt;123456789:AAExxxxxxxx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Send any message to your new bot. Until you do, it cannot write to you first.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;https://api.telegram.org/bot&amp;lt;TOKEN&amp;gt;/getUpdates&lt;/code&gt; in a browser and find &lt;code&gt;"chat":{"id": ...&lt;/code&gt; in the response. That's your &lt;code&gt;chat_id&lt;/code&gt;. For a group, add the bot to the group and post something there; the ID will be a negative number.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: install
&lt;/h2&gt;

&lt;p&gt;You need Docker and Docker Compose. Download the kit: &lt;a href="https://devkit-shop.surge.sh/free/devops-monitoring-lite-v1.0.zip" rel="noopener noreferrer"&gt;https://devkit-shop.surge.sh/free/devops-monitoring-lite-v1.0.zip&lt;/a&gt;, unpack it, and run the installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip devops-monitoring-lite-v1.0.zip
&lt;span class="nb"&gt;cd &lt;/span&gt;devops-monitoring-lite
&lt;span class="nv"&gt;TG_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123456789:AAExxxxxxxx &lt;span class="nv"&gt;TG_CHAT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;987654321 ./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TG_BOT_TOKEN&lt;/code&gt; and &lt;code&gt;TG_CHAT_ID&lt;/code&gt; are only needed for this run: the installer saves them to &lt;code&gt;.env&lt;/code&gt; as &lt;code&gt;TELEGRAM_BOT_TOKEN&lt;/code&gt; and &lt;code&gt;TELEGRAM_CHAT_ID&lt;/code&gt;. If you don't pass them and they aren't in &lt;code&gt;.env&lt;/code&gt; yet, the script asks for the token and chat_id interactively. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;checks for Docker and &lt;code&gt;docker compose&lt;/code&gt; (or the older &lt;code&gt;docker-compose&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;creates &lt;code&gt;.env&lt;/code&gt; and generates a Grafana admin password in it;&lt;/li&gt;
&lt;li&gt;validates the token and chat_id format so a malformed value can't break the YAML;&lt;/li&gt;
&lt;li&gt;renders the Alertmanager config from a template with your token and chat_id;&lt;/li&gt;
&lt;li&gt;runs &lt;code&gt;docker compose up -d&lt;/code&gt; and restarts Alertmanager so it picks up the fresh config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can safely re-run it: the Grafana password and other values in &lt;code&gt;.env&lt;/code&gt; are kept. To change the token, run the installer again with a new &lt;code&gt;TG_BOT_TOKEN&lt;/code&gt;, or edit &lt;code&gt;.env&lt;/code&gt; and run &lt;code&gt;./install.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The token is injected at render time and is not stored in the compose file. The generated &lt;code&gt;alertmanager.yml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; are both listed in the kit's &lt;code&gt;.gitignore&lt;/code&gt;, so they stay out of git.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: reach Grafana
&lt;/h2&gt;

&lt;p&gt;All ports are bound to &lt;code&gt;127.0.0.1&lt;/code&gt; on purpose. Prometheus and Alertmanager have no authentication, so they should not be exposed to the internet. From your computer, use an SSH tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-L&lt;/span&gt; 3000:127.0.0.1:3000 user@your-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;. The login is &lt;code&gt;admin&lt;/code&gt;; the password is in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;GRAFANA_ADMIN_PASSWORD .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the "Host Overview — Сервер" dashboard in the "Monitoring Lite" folder. It shows CPU, memory, disk and network graphs, load average and uptime (the archive includes a sample screenshot, &lt;code&gt;host-overview.png&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;You can open access by IP by setting &lt;code&gt;BIND_ADDR=0.0.0.0&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; and running &lt;code&gt;docker compose up -d&lt;/code&gt;. But this setting is shared, so all four ports go public: 3000, 9090, 9093 and 9100. There's a second trap: ports published by Docker bypass ufw rules, so &lt;code&gt;ufw deny 9090&lt;/code&gt; won't close them. You'd have to block them with your hosting provider's firewall or with rules in the iptables &lt;code&gt;DOCKER-USER&lt;/code&gt; chain. If in doubt, keep &lt;code&gt;127.0.0.1&lt;/code&gt; and use the tunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: verify the alert path
&lt;/h2&gt;

&lt;p&gt;First, check that Prometheus sees the targets and has loaded the rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9090/api/v1/targets | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"health":"[a-z]*"'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9090/api/v1/rules | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"name":"[A-Za-z]*"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both targets (Prometheus itself and node-exporter) should report &lt;code&gt;"health":"up"&lt;/code&gt;, and the second command should list five rule names. Now send a test alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/send-test-alert.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script POSTs to the Alertmanager API. Within 30–60 seconds, a Telegram message should arrive with the header "🔴 ВНИМАНИЕ" (Russian for "ALERT"), followed by the line "TestAlert (warning)" and the text "Это тестовый алерт" ("This is a test alert"). The delay comes from &lt;code&gt;group_wait: 30s&lt;/code&gt;, which lets Alertmanager batch related alerts into one message.&lt;/p&gt;

&lt;p&gt;If nothing arrives, check the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs alertmanager | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; telegram
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most often the cause is that you never messaged the bot, or the token has a typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules
&lt;/h2&gt;

&lt;p&gt;The rules live in &lt;code&gt;config/prometheus/alert_rules.yml&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;&lt;code&gt;for&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InstanceDown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;up{job="node-exporter"} == 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HighCPU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;average CPU load &amp;gt; 90%&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HighMemory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;more than 90% of RAM used, based on &lt;code&gt;MemAvailable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DiskSpace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;disk more than 85% full&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HostRebooted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time() - node_boot_time_seconds &amp;lt; 600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;for&lt;/code&gt; clause prevents false alarms: a short CPU spike during a build or a backup won't wake you up at night. &lt;code&gt;HostRebooted&lt;/code&gt; has no &lt;code&gt;for&lt;/code&gt;, so it fires right after boot and stays active for the first 10 minutes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DiskSpace&lt;/code&gt; rule ignores &lt;code&gt;tmpfs&lt;/code&gt;, &lt;code&gt;overlay&lt;/code&gt;, &lt;code&gt;squashfs&lt;/code&gt;, &lt;code&gt;devtmpfs&lt;/code&gt;, all &lt;code&gt;fuse.*&lt;/code&gt; types (such as &lt;code&gt;fuse.sshfs&lt;/code&gt;), &lt;code&gt;nfs&lt;/code&gt; and &lt;code&gt;nfs4&lt;/code&gt;, &lt;code&gt;cifs&lt;/code&gt;, &lt;code&gt;smb3&lt;/code&gt; and &lt;code&gt;autofs&lt;/code&gt;. Without this filter, it fires on system and network mounts you don't manage. For memory, the rule uses &lt;code&gt;MemAvailable&lt;/code&gt; rather than &lt;code&gt;MemFree&lt;/code&gt;: Linux uses spare memory for cache, so "free" memory is almost never large.&lt;/p&gt;

&lt;p&gt;Thresholds are edited in the same file. After a change, reload the rules without restarting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:9090/-/reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because Prometheus runs with &lt;code&gt;--web.enable-lifecycle&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a second server
&lt;/h2&gt;

&lt;p&gt;Run node-exporter on the second server and add it to &lt;code&gt;config/prometheus/prometheus.yml&lt;/code&gt; under the &lt;code&gt;node-exporter&lt;/code&gt; job as a separate entry with its own label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-exporter&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node-exporter:9100"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;instance_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main-server"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.5:9100"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;instance_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server-2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the &lt;code&gt;instance_name&lt;/code&gt; label on the first server as is; it's already in the kit's config. Then run &lt;code&gt;docker compose restart prometheus&lt;/code&gt;. Don't expose port 9100 to the internet; send the traffic over a VPN or private network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Grafana container won't start.&lt;/strong&gt; Check &lt;code&gt;docker compose logs grafana&lt;/code&gt;. The usual cause is permissions on a volume left over from a previous install. If you don't need the data, &lt;code&gt;./uninstall.sh --purge-data&lt;/code&gt; followed by &lt;code&gt;./install.sh&lt;/code&gt; helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Prometheus target is DOWN.&lt;/strong&gt; Open the targets page through the tunnel at &lt;code&gt;http://127.0.0.1:9090/targets&lt;/code&gt;; it shows the reason, such as &lt;code&gt;connection refused&lt;/code&gt; or &lt;code&gt;context deadline exceeded&lt;/code&gt;. Usually the service hasn't started yet, or there's a typo in the address in &lt;code&gt;prometheus.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port already in use.&lt;/strong&gt; If Grafana or Prometheus is already running on the server, change the relevant variables in &lt;code&gt;.env&lt;/code&gt; (&lt;code&gt;GRAFANA_PORT&lt;/code&gt;, &lt;code&gt;PROMETHEUS_PORT&lt;/code&gt;, &lt;code&gt;ALERTMANAGER_PORT&lt;/code&gt;, &lt;code&gt;NODE_EXPORTER_PORT&lt;/code&gt;) and run &lt;code&gt;docker compose up -d&lt;/code&gt;. Remember to change the port in the &lt;code&gt;curl&lt;/code&gt; commands and the SSH tunnel from this article too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telegram wasn't configured during install.&lt;/strong&gt; Alertmanager still starts, just without sending anything. Fill in &lt;code&gt;TELEGRAM_BOT_TOKEN&lt;/code&gt; and &lt;code&gt;TELEGRAM_CHAT_ID&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; and run &lt;code&gt;./install.sh&lt;/code&gt; again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping and removing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./uninstall.sh              &lt;span class="c"&gt;# stop, keep data&lt;/span&gt;
./uninstall.sh &lt;span class="nt"&gt;--purge-data&lt;/span&gt; &lt;span class="c"&gt;# stop and delete data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's not in this kit
&lt;/h2&gt;

&lt;p&gt;The kit is deliberately limited to one server. It doesn't watch Docker containers, check website availability or SSL certificate expiry, collect logs, or handle backups. If you need those, you can add them yourself: cAdvisor for containers, blackbox-exporter for sites, Loki for logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full version
&lt;/h2&gt;

&lt;p&gt;The paid DevOps Monitoring Starter Kit covers those gaps: cAdvisor, blackbox-exporter for site and SSL checks, three dashboards (host, Docker containers, sites and SSL), 11 alert rules, a backup script, and optionally Loki with Promtail for logs and HTTPS via Caddy. Details are at &lt;a href="https://devkit-shop.surge.sh/" rel="noopener noreferrer"&gt;https://devkit-shop.surge.sh/&lt;/a&gt;, where you can pay with USDT (TRC20) or, in Russia, by card.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this article and the kit were prepared by an AI assistant working on behalf of the kit's author, and the commands were checked against the kit files.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>prometheus</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
