{"id":39,"date":"2026-07-30T18:23:39","date_gmt":"2026-07-30T15:23:39","guid":{"rendered":"https:\/\/john-nessime.com\/blog\/?p=39"},"modified":"2026-07-30T18:23:41","modified_gmt":"2026-07-30T15:23:41","slug":"prometheus-grafana-one-server","status":"publish","type":"post","link":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/","title":{"rendered":"Prometheus + Grafana From Scratch on One Server"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">You want to know when the disk fills up before a customer tells you. Search for how to do that and you get pointed at a hosted platform with per-host pricing, or a twelve-service Docker Compose file that somebody copy-pasted from a blog and does not fully understand either.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a single server, neither is the right answer. Prometheus and Grafana are two static Go binaries and a package. They run happily under systemd, use very little memory at this scale, and when something breaks you can read the logs and actually fix it. No container networking to debug, no orchestration layer between you and the problem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This walks through the whole thing: install Prometheus and Grafana from scratch, wire in node_exporter, put the lot behind nginx with TLS, and lock down the ports that should never have been public in the first place. Commands work on both Debian-family and RHEL-family systems, with the differences called out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What you are building<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Four moving parts, all on one box:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>node_exporter<\/strong> exposes machine metrics \u2014 CPU, memory, disk, network \u2014 on port 9100.<\/li>\n\n<li><strong>Prometheus<\/strong> scrapes that endpoint on a schedule and stores the samples in its own time series database on port 9090.<\/li>\n\n<li><strong>Grafana<\/strong> queries Prometheus and draws the dashboards on port 3000.<\/li>\n\n<li><strong>nginx<\/strong> terminates TLS and is the only thing listening on a public interface.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The critical design decision is in that last line. Prometheus and node_exporter ship with no authentication at all. Bound to a public interface, they hand your entire infrastructure inventory to anyone who runs a port scan. Everything binds to 127.0.0.1 and nginx is the single front door.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Resource-wise, a 2 GB VPS handles this comfortably for one server&#8217;s worth of metrics. Prometheus is the memory-hungry one, and its appetite scales with the number of active time series rather than the number of hosts.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Users and directories<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Neither service should run as root, and neither needs a login shell.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo useradd --system --no-create-home --shell \/sbin\/nologin prometheus\nsudo useradd --system --no-create-home --shell \/sbin\/nologin node_exporter\n\nsudo mkdir -p \/etc\/prometheus \/var\/lib\/prometheus\nsudo chown prometheus:prometheus \/etc\/prometheus \/var\/lib\/prometheus<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On Debian and Ubuntu the nologin shell lives at <code>\/usr\/sbin\/nologin<\/code> rather than <code>\/sbin\/nologin<\/code>. Check with <code>which nologin<\/code> if useradd complains.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Install Prometheus<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prometheus ships as a single static binary. Rather than hardcoding a version that will be stale next month, pull the current release tag from the API:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PROM_VERSION=$(curl -s https:\/\/api.github.com\/repos\/prometheus\/prometheus\/releases\/latest \n  | grep -Po '\"tag_name\": \"vK[^\"]*')\necho \"Installing Prometheus ${PROM_VERSION}\"\n\ncd \/tmp\ncurl -LO \"https:\/\/github.com\/prometheus\/prometheus\/releases\/download\/v${PROM_VERSION}\/prometheus-${PROM_VERSION}.linux-amd64.tar.gz\"\ntar xzf \"prometheus-${PROM_VERSION}.linux-amd64.tar.gz\"\ncd \"prometheus-${PROM_VERSION}.linux-amd64\"\n\nsudo cp prometheus promtool \/usr\/local\/bin\/\nsudo chown prometheus:prometheus \/usr\/local\/bin\/prometheus \/usr\/local\/bin\/promtool<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you are on ARM, swap <code>linux-amd64<\/code> for <code>linux-arm64<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The configuration file<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Write <code>\/etc\/prometheus\/prometheus.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>global:\n  scrape_interval: 15s\n  evaluation_interval: 15s\n\nscrape_configs:\n  - job_name: prometheus\n    static_configs:\n      - targets: [\"127.0.0.1:9090\"]\n\n  - job_name: node\n    static_configs:\n      - targets: [\"127.0.0.1:9100\"]\n        labels:\n          instance: \"web-01\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Set that <code>instance<\/code> label deliberately. It follows every metric into every dashboard and alert, and renaming it later means your historical graphs split in two.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Validate before you start anything:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>promtool check config \/etc\/prometheus\/prometheus.yml<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The systemd unit<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Write <code>\/etc\/systemd\/system\/prometheus.service<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=Prometheus\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nUser=prometheus\nGroup=prometheus\nType=simple\nRestart=on-failure\nRestartSec=5\nExecStart=\/usr\/local\/bin\/prometheus \n  --config.file=\/etc\/prometheus\/prometheus.yml \n  --storage.tsdb.path=\/var\/lib\/prometheus \n  --storage.tsdb.retention.time=30d \n  --storage.tsdb.retention.size=8GB \n  --web.listen-address=127.0.0.1:9090\nExecReload=\/bin\/kill -HUP $MAINPID\n\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=true\nReadWritePaths=\/var\/lib\/prometheus\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two things worth noticing. <code>--web.listen-address=127.0.0.1:9090<\/code> is what keeps Prometheus off the public internet. And setting <em>both<\/em> retention flags matters \u2014 time-based retention alone will not save you if metric volume spikes, because Prometheus honours whichever limit it hits first.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now prometheus\nsudo systemctl status prometheus<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: node_exporter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Same pattern \u2014 a static binary and a unit file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>NE_VERSION=$(curl -s https:\/\/api.github.com\/repos\/prometheus\/node_exporter\/releases\/latest \n  | grep -Po '\"tag_name\": \"vK[^\"]*')\n\ncd \/tmp\ncurl -LO \"https:\/\/github.com\/prometheus\/node_exporter\/releases\/download\/v${NE_VERSION}\/node_exporter-${NE_VERSION}.linux-amd64.tar.gz\"\ntar xzf \"node_exporter-${NE_VERSION}.linux-amd64.tar.gz\"\nsudo cp \"node_exporter-${NE_VERSION}.linux-amd64\/node_exporter\" \/usr\/local\/bin\/\nsudo chown node_exporter:node_exporter \/usr\/local\/bin\/node_exporter<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then <code>\/etc\/systemd\/system\/node_exporter.service<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=Prometheus Node Exporter\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nUser=node_exporter\nGroup=node_exporter\nType=simple\nRestart=on-failure\nExecStart=\/usr\/local\/bin\/node_exporter \n  --web.listen-address=127.0.0.1:9100\n\nNoNewPrivileges=true\nProtectHome=true\nPrivateTmp=true\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl daemon-reload\nsudo systemctl enable --now node_exporter\n\n# Confirm it is actually producing metrics\ncurl -s http:\/\/127.0.0.1:9100\/metrics | head -20<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now open <code>http:\/\/127.0.0.1:9090\/targets<\/code> \u2014 via an SSH tunnel, since it is not public \u2014 and both jobs should read UP.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># From your laptop\nssh -L 9090:127.0.0.1:9090 you@your-server<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Install Grafana<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Grafana is packaged properly, so use the official repository rather than a tarball. It gets you signed updates through your normal patching process.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Debian and Ubuntu:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt-get install -y apt-transport-https software-properties-common wget\nsudo mkdir -p \/etc\/apt\/keyrings\/\nwget -q -O - https:\/\/apt.grafana.com\/gpg.key \n  | gpg --dearmor | sudo tee \/etc\/apt\/keyrings\/grafana.gpg &gt; \/dev\/null\n\necho \"deb [signed-by=\/etc\/apt\/keyrings\/grafana.gpg] https:\/\/apt.grafana.com stable main\" \n  | sudo tee \/etc\/apt\/sources.list.d\/grafana.list\n\nsudo apt-get update\nsudo apt-get install -y grafana<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>RHEL, AlmaLinux, Rocky:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tee \/etc\/yum.repos.d\/grafana.repo &gt; \/dev\/null &lt;&lt;'EOF'\n[grafana]\nname=grafana\nbaseurl=https:\/\/rpm.grafana.com\nrepo_gpgcheck=1\nenabled=1\ngpgcheck=1\ngpgkey=https:\/\/rpm.grafana.com\/gpg.key\nsslverify=1\nsslcacert=\/etc\/pki\/tls\/certs\/ca-bundle.crt\nEOF\n\nsudo dnf install -y grafana<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Bind it to localhost too<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Edit <code>\/etc\/grafana\/grafana.ini<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>[server]\nhttp_addr = 127.0.0.1\nhttp_port = 3000\ndomain = metrics.example.com\nroot_url = https:\/\/metrics.example.com\/\n\n[security]\nadmin_user = admin\nadmin_password = put-something-real-here\ncookie_secure = true\n\n[users]\nallow_sign_up = false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Getting <code>root_url<\/code> right matters more than it looks. Set it wrong and Grafana generates broken links and redirects behind the proxy, which produces a confusing half-working UI rather than a clean error.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now grafana-server<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Put nginx and TLS in front<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the only piece that faces the internet.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server {\n    listen 443 ssl;\n    server_name metrics.example.com;\n\n    ssl_certificate     \/etc\/letsencrypt\/live\/metrics.example.com\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/metrics.example.com\/privkey.pem;\n\n    location \/ {\n        proxy_pass http:\/\/127.0.0.1:3000;\n        proxy_set_header Host              $host;\n        proxy_set_header X-Real-IP         $remote_addr;\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n    # Grafana Live needs websockets, and breaks silently without this\n    location \/api\/live\/ {\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade    $http_upgrade;\n        proxy_set_header Connection \"Upgrade\";\n        proxy_set_header Host       $host;\n        proxy_pass http:\/\/127.0.0.1:3000;\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note <code>fullchain.pem<\/code> rather than <code>cert.pem<\/code>. Getting that wrong produces a site that works in your browser and fails for anything else.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then close the metrics ports at the firewall, belt and braces:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># firewalld\nsudo firewall-cmd --permanent --add-service=https\nsudo firewall-cmd --reload\n\n# ufw\nsudo ufw allow 443\/tcp\n\n# Verify nothing unexpected is listening publicly\nsudo ss -tlnp | grep -E '9090|9100|3000'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every line of that last command should show <code>127.0.0.1<\/code>. If you see <code>0.0.0.0<\/code> or <code>*<\/code>, something is exposed and you should fix it before going any further.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Connect Grafana to Prometheus<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can click through the UI, but provisioning it as a file means a rebuilt server comes back identical. Create <code>\/etc\/grafana\/provisioning\/datasources\/prometheus.yml<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apiVersion: 1\n\ndatasources:\n  - name: Prometheus\n    type: prometheus\n    access: proxy\n    url: http:\/\/127.0.0.1:9090\n    isDefault: true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Restart Grafana, log in, and import dashboard ID <strong>1860<\/strong> from the Grafana dashboard library \u2014 Node Exporter Full. It is comprehensive, well maintained, and will show you more about your server than you knew was measurable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Sizing the disk before it bites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prometheus storage is predictable. Roughly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>disk = retention_seconds \u00d7 samples_per_second \u00d7 bytes_per_sample<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Compressed samples land in the region of one to two bytes each. Measure your actual rate rather than guessing \u2014 run this in the Prometheus query box:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>rate(prometheus_tsdb_head_samples_appended_total[5m])<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One server with node_exporter at a 15 second interval is a small number, and 30 days fits in a few gigabytes. The number climbs fast once you add exporters, so keep <code>--storage.tsdb.retention.size<\/code> set as a hard ceiling. Prometheus filling the root partition takes the rest of the box down with it, which is a memorable way to discover your monitoring has no monitoring.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">A target shows DOWN<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Work outward from the exporter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -s http:\/\/127.0.0.1:9100\/metrics | head\nsudo systemctl status node_exporter\nsudo journalctl -u prometheus -n 50 --no-pager<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On RHEL-family systems with SELinux enforcing, this is frequently SELinux rather than your config. Check before you start changing things:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ausearch -m avc -ts recent<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If that shows denials, write a policy for them. Do not reach for <code>setenforce 0<\/code> \u2014 it fixes today&#8217;s symptom and quietly removes a layer of protection from the whole machine.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Grafana loads but every panel says &#8220;No data&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Almost always the datasource URL. It must be <code>http:\/\/127.0.0.1:9090<\/code> from Grafana&#8217;s point of view, not your public hostname. Test the datasource from its settings page \u2014 Grafana reports the actual connection error, which is more useful than the empty panel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Prometheus will not start<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u prometheus -n 50 --no-pager\npromtool check config \/etc\/prometheus\/prometheus.yml\nls -la \/var\/lib\/prometheus<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The usual culprits are a YAML indentation error, or <code>\/var\/lib\/prometheus<\/code> not being owned by the prometheus user after a manual file copy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Broken styling or redirect loops behind the proxy<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>root_url<\/code> in grafana.ini does not match how you are actually reaching the site. If you are serving from a subpath, you also need <code>serve_from_sub_path = true<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common mistakes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Leaving 9090, 9100 or 3000 on a public interface. Prometheus has no authentication whatsoever.<\/li>\n\n<li>Keeping the default Grafana admin password because it prompts you to change it and you clicked past.<\/li>\n\n<li>Setting no retention limits and discovering the problem when the root partition fills.<\/li>\n\n<li>Dropping <code>scrape_interval<\/code> to 1s because more data seems better. It multiplies storage and buys you nothing at this scale.<\/li>\n\n<li>Never backing up <code>\/var\/lib\/grafana\/grafana.db<\/code>, which holds every dashboard you have built.<\/li>\n\n<li>Running everything as root because the tutorial you followed did.<\/li>\n\n<li>Building dashboards but configuring no alerts, so you still find out from a user.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Bind every component to localhost and expose exactly one TLS endpoint.<\/li>\n\n<li>Set both time and size retention. Whichever triggers first is the one you want.<\/li>\n\n<li>Provision datasources and dashboards as files, so the config lives in version control.<\/li>\n\n<li>Back up the Grafana SQLite database on the same schedule as everything else that matters.<\/li>\n\n<li>Add an external uptime check. Self-hosted monitoring cannot tell you that the server it lives on has gone.<\/li>\n\n<li>Write a handful of alerts you will genuinely act on. Alerts nobody responds to train people to ignore alerts.<\/li>\n\n<li>Use systemd&#8217;s sandboxing directives. They cost nothing and limit what a compromised exporter can reach.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Should I use Docker instead?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For a single server, systemd is simpler. Two static binaries and a package, with no container networking layer between you and the logs. Containers earn their keep once you are managing several hosts or already run everything else that way.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How much RAM does this need?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A 2 GB VPS is comfortable for one server. Prometheus memory tracks active time series rather than host count, so it grows when you add exporters, not when you add machines with the same exporters.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I monitor more servers later?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Install node_exporter on each and add them as targets. The moment metrics cross a network, though, that traffic needs protecting \u2014 a VPN, an SSH tunnel, or TLS with client certificates. Do not simply open 9100 to the world.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where do alerts come from?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Two options. Grafana&#8217;s built-in alerting works from dashboard queries and is the quicker path. Alertmanager, run alongside Prometheus, is more powerful for routing, grouping and silencing. Start with Grafana&#8217;s and move if you outgrow it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How long should I keep metrics?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Thirty days covers most troubleshooting and capacity questions. Longer retention on a single box is usually a sign you want a remote-write backend rather than a bigger disk.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need node_exporter if I only care about my application?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Install it anyway. When the application misbehaves, the first question is whether the machine underneath it is healthy, and node_exporter is what answers that.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you install Prometheus and Grafana this way, the whole stack is four systemd units and three config files. Everything binds to localhost, nginx handles TLS, and there is no layer of abstraction between you and a problem when something breaks at an inconvenient hour.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Get the dashboard working first, then spend an evening writing three or four alerts you will actually respond to. A dashboard tells you what happened when you go looking. An alert is what stops you needing to look.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Want this set up properly?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I build monitoring stacks for teams who would rather not spend a weekend on it \u2014 Prometheus and Grafana on bare servers or in Kubernetes, exporters for the things you actually run, alert rules tuned so they mean something, and dashboards that answer real questions rather than filling a screen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If that sounds useful, you can hire me on Upwork.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.upwork.com\/freelancers\/~01f15a912ad84a6620\" target=\"_blank\" rel=\"noreferrer noopener\">Work with me on Upwork<\/a><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Two static binaries, a package and four systemd units. How to install Prometheus and Grafana on a single server from scratch, wire in node_exporter, put nginx and TLS in front, and keep the ports that have no authentication off the public internet.<\/p>\n","protected":false},"author":1,"featured_media":40,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,26,63],"tags":[98,3,11,21,6,13,18,96,16,22,72,97],"class_list":["post-39","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-linux","category-system-administration","tag-alerting","tag-devops","tag-grafana","tag-infrastructure","tag-linux","tag-monitoring","tag-nginx","tag-observability","tag-prometheus","tag-self-hosting","tag-sysadmin","tag-systemd","entry","has-media"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Install Prometheus and Grafana on One Server<\/title>\n<meta name=\"description\" content=\"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Install Prometheus and Grafana on One Server\" \/>\n<meta property=\"og:description\" content=\"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/\" \/>\n<meta property=\"og:site_name\" content=\"John Nessime\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-30T15:23:39+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-30T15:23:41+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"John Nessime\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"John Nessime\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"7 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/\"},\"author\":{\"name\":\"John Nessime\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"headline\":\"Prometheus + Grafana From Scratch on One Server\",\"datePublished\":\"2026-07-30T15:23:39+00:00\",\"dateModified\":\"2026-07-30T15:23:41+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/\"},\"wordCount\":1544,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/prometheus-grafana-one-server-featured.png\",\"keywords\":[\"Alerting\",\"DevOps\",\"Grafana\",\"Infrastructure\",\"Linux\",\"Monitoring\",\"Nginx\",\"Observability\",\"Prometheus\",\"Self Hosting\",\"Sysadmin\",\"Systemd\"],\"articleSection\":[\"DevOps\",\"Linux\",\"System Administration\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/\",\"name\":\"Install Prometheus and Grafana on One Server\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/prometheus-grafana-one-server-featured.png\",\"datePublished\":\"2026-07-30T15:23:39+00:00\",\"dateModified\":\"2026-07-30T15:23:41+00:00\",\"description\":\"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#primaryimage\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/prometheus-grafana-one-server-featured.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/prometheus-grafana-one-server-featured.png\",\"width\":1200,\"height\":800,\"caption\":\"Grafana style dashboard showing CPU and memory time series with stat panels, alongside the scrape flow from node_exporter through Prometheus and Grafana to nginx with TLS\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/prometheus-grafana-one-server\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Prometheus + Grafana From Scratch on One Server\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\",\"name\":\"John Nessime\",\"description\":\"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained\",\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\",\"name\":\"John Nessime\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"width\":512,\"height\":512,\"caption\":\"John Nessime\"},\"logo\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\"},\"description\":\"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.\",\"sameAs\":[\"https:\\\/\\\/john-nessime.com\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/john-m-nessime\"],\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/author\\\/johnnessime\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Install Prometheus and Grafana on One Server","description":"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/","og_locale":"en_US","og_type":"article","og_title":"Install Prometheus and Grafana on One Server","og_description":"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.","og_url":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/","og_site_name":"John Nessime","article_published_time":"2026-07-30T15:23:39+00:00","article_modified_time":"2026-07-30T15:23:41+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png","type":"image\/png"}],"author":"John Nessime","twitter_card":"summary_large_image","twitter_misc":{"Written by":"John Nessime","Est. reading time":"7 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#article","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/"},"author":{"name":"John Nessime","@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"headline":"Prometheus + Grafana From Scratch on One Server","datePublished":"2026-07-30T15:23:39+00:00","dateModified":"2026-07-30T15:23:41+00:00","mainEntityOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/"},"wordCount":1544,"commentCount":0,"publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png","keywords":["Alerting","DevOps","Grafana","Infrastructure","Linux","Monitoring","Nginx","Observability","Prometheus","Self Hosting","Sysadmin","Systemd"],"articleSection":["DevOps","Linux","System Administration"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/","url":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/","name":"Install Prometheus and Grafana on One Server","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#primaryimage"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png","datePublished":"2026-07-30T15:23:39+00:00","dateModified":"2026-07-30T15:23:41+00:00","description":"Install Prometheus and Grafana on one server from scratch: node_exporter, systemd units, nginx with TLS, retention sizing, and the ports to never expose.","breadcrumb":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#primaryimage","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/prometheus-grafana-one-server-featured.png","width":1200,"height":800,"caption":"Grafana style dashboard showing CPU and memory time series with stat panels, alongside the scrape flow from node_exporter through Prometheus and Grafana to nginx with TLS"},{"@type":"BreadcrumbList","@id":"https:\/\/john-nessime.com\/blog\/devops\/prometheus-grafana-one-server\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/john-nessime.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Prometheus + Grafana From Scratch on One Server"}]},{"@type":"WebSite","@id":"https:\/\/john-nessime.com\/blog\/#website","url":"https:\/\/john-nessime.com\/blog\/","name":"John Nessime","description":"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained","publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/john-nessime.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105","name":"John Nessime","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","width":512,"height":512,"caption":"John Nessime"},"logo":{"@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png"},"description":"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.","sameAs":["https:\/\/john-nessime.com\/blog","https:\/\/www.linkedin.com\/in\/john-m-nessime"],"url":"https:\/\/john-nessime.com\/blog\/author\/johnnessime\/"}]}},"_links":{"self":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/39","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/comments?post=39"}],"version-history":[{"count":1,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/39\/revisions"}],"predecessor-version":[{"id":41,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/39\/revisions\/41"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media\/40"}],"wp:attachment":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media?parent=39"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/categories?post=39"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/tags?post=39"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}