 {"id":521853,"date":"2026-07-12T15:04:04","date_gmt":"2026-07-12T22:04:04","guid":{"rendered":"https:\/\/jorgep.com\/blog\/?p=521853"},"modified":"2026-09-03T15:11:57","modified_gmt":"2026-09-03T22:11:57","slug":"setting-up-proactive-vps-management","status":"publish","type":"post","link":"https:\/\/jorgep.com\/blog\/setting-up-proactive-vps-management\/","title":{"rendered":"Setting up Proactive VPS Management"},"content":{"rendered":"<style>.wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7, .wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7[data-kb-block=\"kb-adv-heading407818_afcbba-c7\"]{font-size:var(--global-kb-font-size-sm, 0.9rem);font-style:normal;}.wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7 mark.kt-highlight, .wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7[data-kb-block=\"kb-adv-heading407818_afcbba-c7\"] mark.kt-highlight{font-style:normal;color:#f76a0c;-webkit-box-decoration-break:clone;box-decoration-break:clone;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;}.wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7 img.kb-inline-image, .wp-block-kadence-advancedheading.kt-adv-heading407818_afcbba-c7[data-kb-block=\"kb-adv-heading407818_afcbba-c7\"] img.kb-inline-image{width:150px;vertical-align:baseline;}<\/style>\n<p class=\"kt-adv-heading407818_afcbba-c7 wp-block-kadence-advancedheading\" data-kb-block=\"kb-adv-heading407818_afcbba-c7\">Have questions, ideas to share, or just want to connect? I\u2019d love to hear from you! Check out my <a href=\"https:\/\/jorgep.com\/blog\/about\/\">About Page<\/a> to learn more about me or connect with me.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are anything like me, it all started with one Virtual Private Server (VPS) and before you close your eyes, you have many of them split between the cloud and your local environment. I use Docker containers for most of my work, and this past weekend one of the VPS was shut down because of a runaway CPU Utilization spiked at over 95% &#8211; it was an easy restore &#8211; I just had to start it again, but caused some disruption of course!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So what do you need:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>Malware Detection:<\/strong> Filesystem scanners (like Hostinger\u2019s Monarx or standalone security agents) to ensure high CPU isn&#8217;t caused by a compromised script, webshell, or crypto-miner.<\/li>\n\n\n\n<li><strong>Resource Monitor (and logger):<\/strong> A time-series metric collector to record CPU, memory, and I\/O history per container so you can inspect what spiked <em>after<\/em> the fact.<\/li>\n\n\n\n<li><strong>Centralized VPS\/Container Manager:<\/strong> A single interface (like Portainer with remote Agents) to manage containers, review logs, and control stacks across all your cloud and local VPS instances from one dashboard.<\/li>\n\n\n\n<li><strong>Centralized Prometheus\/Grafana:<\/strong> A unified monitoring setup to pull performance metrics across all your distributed nodes into a single, comprehensive dashboard.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">While malware scanners handle security and Portainer handles container management, you need a dedicated telemetry stack for historical resource logging.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Installing cAdvisor + Prometheus + Grafana via Docker Compose<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This stack uses <strong>cAdvisor<\/strong> to collect container metrics, <strong>Prometheus<\/strong> to store them over time, and <strong>Grafana<\/strong> to visualize CPU trends and set up alerts.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Step 1: Create the Project Directory<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Connect to your VPS via SSH or terminal and create a dedicated directory for your monitoring configuration:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p \/your-folder\/monitoring\ncd \/your-folder\/monitoring\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Step 2: Configure Prometheus<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>prometheus.yml<\/code> to instruct Prometheus to scrape metrics from cAdvisor every 15 seconds:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat &gt; prometheus.yml &lt;&lt;'EOF'\nglobal:\n  scrape_interval: 15s\n\nscrape_configs:\n  - job_name: 'cadvisor'\n    static_configs:\n      - targets: &#91;'cadvisor:8080']\nEOF\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Step 3: Create the Docker Compose Deployment<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>docker-compose.yml<\/code> to define the three monitoring services:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat &gt; docker-compose.yml &lt;&lt;'EOF'\nservices:\n  cadvisor:\n    image: ghcr.io\/google\/cadvisor:latest\n    container_name: cadvisor\n    restart: unless-stopped\n    command:\n      - --docker_only=true\n    volumes:\n      - \/:\/rootfs:ro\n      - \/var\/run:\/var\/run:ro\n      - \/sys:\/sys:ro\n      - \/var\/lib\/docker:\/var\/lib\/docker:ro\n    networks:\n      - monitoring\n\n  prometheus:\n    image: prom\/prometheus:latest\n    container_name: prometheus\n    restart: unless-stopped\n    command:\n      - --config.file=\/etc\/prometheus\/prometheus.yml\n      - --storage.tsdb.retention.time=30d\n    volumes:\n      - .\/prometheus.yml:\/etc\/prometheus\/prometheus.yml:ro\n      - prometheus-data:\/prometheus\n    ports:\n      - \"127.0.0.1:9090:9090\"\n    networks:\n      - monitoring\n\n  grafana:\n    image: grafana\/grafana-oss:latest\n    container_name: grafana\n    restart: unless-stopped\n    volumes:\n      - grafana-data:\/var\/lib\/grafana\n    ports:\n      - \"127.0.0.1:3000:3000\"\n    networks:\n      - monitoring\n\nvolumes:\n  prometheus-data:\n  grafana-data:\n\nnetworks:\n  monitoring:\nEOF\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Step 4: Start the Stack &amp; Access the Dashboard Securely<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Launch the containers in detached mode:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose up -d\ndocker compose ps\n<\/code><\/pre>\n\n\n\n<h5 class=\"wp-block-heading\">ON YOUR LAPTOP: (Not on your server!) Do the following: <\/h5>\n\n\n\n<p class=\"wp-block-paragraph\">To keep management ports hidden from the public internet, open a secure SSH tunnel from your local computer:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -L 3000:127.0.0.1:3000 root@YOUR_VPS_IP\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open your web browser to <code>[http:\/\/127.0.0.1:3000](http:\/\/127.0.0.1:3000)<\/code>. Sign in with the default credentials (<code>admin<\/code> \/ <code>admin<\/code>) and set a new password when prompted.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Connecting Grafana to Prometheus &amp; Tracking CPU Spikes<\/h3>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>In Grafana, navigate to <strong>Connections<\/strong> \u2192 <strong>Data Sources<\/strong> \u2192 <strong>Add Data Source<\/strong>.<\/li>\n\n\n\n<li>Select <strong>Prometheus<\/strong>.<\/li>\n\n\n\n<li>Set the Connection URL to <code>http:\/\/prometheus:9090<\/code> and click <strong>Save &amp; Test<\/strong>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">To build a panel that tracks the exact CPU percentage utilized by each container, create a Grafana visualization using this PromQL query:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open Grafana at\u00a0<code>http:\/\/127.0.0.1:3000<\/code>.<\/li>\n\n\n\n<li>In the left menu, open\u00a0<strong>Explore<\/strong>.<\/li>\n\n\n\n<li>Select your\u00a0<strong>Prometheus<\/strong>\u00a0data source.<\/li>\n\n\n\n<li>Paste the query into the query editor and click\u00a0<strong>Run queries<\/strong>:<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sum by (name) (\n  rate(container_cpu_usage_seconds_total{name!=\"\"}&#91;5m])\n) * 100\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For a permanent graph, go to\u00a0<strong>Dashboards \u2192 New \u2192 New dashboard \u2192 Add visualization<\/strong>, select Prometheus, paste the same query, then click\u00a0<strong>Apply<\/strong>\u00a0and save the dashboard. If it returns no data, check that the Prometheus target for\u00a0<code>cadvisor:8080<\/code>\u00a0is healthy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With this query running, the next time a container triggers a sudden workload or runaway loop, you won&#8217;t have to guess\u2014you can open Grafana, look back at the exact time of the spike, and identify the responsible container right away.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once setup you will be able to access something like this: <\/p>\n\n\n\n<figure data-wp-context=\"{&quot;galleryId&quot;:&quot;6a9a4a8f87ba5&quot;}\" data-wp-interactive=\"core\/gallery\" class=\"wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex\">\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6a9a4a8f8a041&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6a9a4a8f8a041\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"482\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" data-id=\"521856\" src=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194-1024x482.png\" alt=\"\" class=\"wp-image-521856\" srcset=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194-1024x482.png 1024w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194-766x361.png 766w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194-1536x724.png 1536w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194-300x141.png 300w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-194.png 1679w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6a9a4a8f8a93a&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6a9a4a8f8a93a\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"541\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" data-id=\"521855\" src=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193-1024x541.png\" alt=\"\" class=\"wp-image-521855\" srcset=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193-1024x541.png 1024w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193-766x405.png 766w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193-1536x812.png 1536w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193-300x159.png 300w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-193.png 1724w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure>\n<\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>cAdvisor is running and Prometheus is successfully scraping it<\/strong>, Grafana will show CPU utilization history for each container.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Important details:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>History starts\u00a0<strong>when Prometheus begins collecting metrics<\/strong>; it cannot show CPU usage from before installation.<\/li>\n\n\n\n<li>The Compose file I provided keeps approximately\u00a0<strong>30 days<\/strong>\u00a0of Prometheus data.<\/li>\n\n\n\n<li>The query displays average CPU usage over the selected Grafana time range.<\/li>\n\n\n\n<li>It shows container CPU usage, but not necessarily VPS\u00a0<strong>CPU steal (<code>%st<\/code>)<\/strong>. Keep checking host-level metrics separately if you need to distinguish container load from physical CPU contention.<\/li>\n\n\n\n<li>In Grafana, use the time selector in the upper-right corner to view the last hour, day, week, or another range.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>If you are anything like me, it all started with one Virtual Private Server (VPS) and before you close your eyes, you have many of them split between the cloud and your local environment. I use Docker containers for most of my work, and this past weekend one of the VPS was shut down because&#8230;<\/p>\n","protected":false},"author":2,"featured_media":521018,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_kad_blocks_custom_css":"","_kad_blocks_head_custom_js":"","_kad_blocks_body_custom_js":"","_kad_blocks_footer_custom_js":"","ngg_post_thumbnail":0,"episode_type":"","audio_file":"","podmotor_file_id":"","podmotor_episode_id":"","cover_image":"","cover_image_id":"","duration":"","filesize":"","filesize_raw":"","date_recorded":"","explicit":"","block":"","itunes_episode_number":"","itunes_title":"","itunes_season_number":"","itunes_episode_type":"","_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[441,446],"tags":[930,894,917,986],"class_list":["post-521853","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech-talk","category-tips-tools-resources","tag-ai-series","tag-artificial-intelligence","tag-containers","tag-local-ai"],"taxonomy_info":{"category":[{"value":441,"label":"Tech Talk"},{"value":446,"label":"Tips, Tools &amp; Resources"}],"post_tag":[{"value":930,"label":"AI Series"},{"value":894,"label":"artificial intelligence"},{"value":917,"label":"Containers"},{"value":986,"label":"Local AI"}]},"featured_image_src_large":["https:\/\/jorgep.com\/blog\/wp-content\/uploads\/jorgepSection-1200x300-LocalAI-b-1024x256.jpg",1024,256,true],"author_info":{"display_name":"Jorge Pereira","author_link":"https:\/\/jorgep.com\/blog\/author\/jorge\/"},"comment_info":0,"category_info":[{"term_id":441,"name":"Tech Talk","slug":"tech-talk","term_group":0,"term_taxonomy_id":451,"taxonomy":"category","description":"","parent":0,"count":750,"filter":"raw","cat_ID":441,"category_count":750,"category_description":"","cat_name":"Tech Talk","category_nicename":"tech-talk","category_parent":0},{"term_id":446,"name":"Tips, Tools &amp; Resources","slug":"tips-tools-resources","term_group":0,"term_taxonomy_id":456,"taxonomy":"category","description":"","parent":0,"count":99,"filter":"raw","cat_ID":446,"category_count":99,"category_description":"","cat_name":"Tips, Tools &amp; Resources","category_nicename":"tips-tools-resources","category_parent":0}],"tag_info":[{"term_id":930,"name":"AI Series","slug":"ai-series","term_group":0,"term_taxonomy_id":940,"taxonomy":"post_tag","description":"","parent":0,"count":239,"filter":"raw"},{"term_id":894,"name":"artificial intelligence","slug":"artificial-intelligence","term_group":0,"term_taxonomy_id":904,"taxonomy":"post_tag","description":"","parent":0,"count":212,"filter":"raw"},{"term_id":917,"name":"Containers","slug":"containers","term_group":0,"term_taxonomy_id":927,"taxonomy":"post_tag","description":"","parent":0,"count":8,"filter":"raw"},{"term_id":986,"name":"Local AI","slug":"local-ai","term_group":0,"term_taxonomy_id":996,"taxonomy":"post_tag","description":"","parent":0,"count":63,"filter":"raw"}],"_links":{"self":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/521853","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/comments?post=521853"}],"version-history":[{"count":2,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/521853\/revisions"}],"predecessor-version":[{"id":521857,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/521853\/revisions\/521857"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/media\/521018"}],"wp:attachment":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/media?parent=521853"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/categories?post=521853"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/tags?post=521853"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}