Skip to content

How to Deploy Prometheus on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Sep 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Prometheus with node_exporter on a VPS using Docker Compose — loopback-only listeners, basic auth, a retention limit you chose on purpose, and HTTPS through Caddy.

Before you start
  • A VPS with 2 GB RAM or more — more if you scrape many targets
  • A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
  • A domain you can point at the server
  • Docker Engine + Compose installed (see the base guide below)
  • Comfort editing YAML — scrape targets live in prometheus.yml
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Prometheus is

Prometheus is a metrics database with its own collector. On a fixed interval it pulls (scrapes) plain-text metrics from HTTP endpoints you list, stores them as time series in a local database (the TSDB), and lets you query them with PromQL. It can evaluate alerting rules against those series. Delivering the alerts (email, Slack, PagerDuty) is the job of a separate component, Alertmanager.

Two things it is not. It is not a dashboard tool: the built-in UI is for running queries and checking targets, and most people put Grafana in front of it for graphs. The Prometheus vs Grafana comparison explains how the two divide the work. It is also not a log or trace store. If you want metrics, logs and traces in one product, look at SigNoz instead.

This guide deploys Prometheus next to node_exporter, the official exporter for host metrics (CPU, memory, disk, network), so you end up with a working target and real data rather than an empty database.

Server sizing

Prometheus's memory and disk use grow with the number of time series you scrape, not with the number of servers. One node_exporter produces around a thousand series. A busy application exporter can produce many more.

  • RAM: our catalog entry lists 2 GB as the minimum for a real deployment. In our install check (GCP e2-standard-2, Ubuntu 26.04, September 2026), Prometheus scraping only itself sat at about 37 MB idle with about 364 MB of Docker disk. That figure is a floor. Every target you add raises it.
  • Disk: the Prometheus docs estimate 1–2 bytes per sample and give the formula retention_time_seconds × ingested_samples_per_second × bytes_per_sample. For example, 10,000 series scraped every 15 seconds is about 667 samples per second. Over 30 days at 2 bytes per sample, that comes to about 3.5 GB. Leave room for the write-ahead log on top.
  • CPU: one vCPU is enough for a single-host setup like this one. Query-heavy dashboards are what use more CPU.

The placement rule for monitoring applies here too: a Prometheus server that lives on the machine it watches goes down with that machine. For anything beyond a personal box, give it its own small VPS. The Best VPS for Monitoring page ranks hosts for this.

Prepare the server

This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first. Open SSH and the reverse proxy ports only:

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose

Prometheus's port 9090 and node_exporter's port 9100 will listen on 127.0.0.1 only, so neither needs a firewall rule. Neither serves anything you want the internet to read.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Write the configuration

Prometheus reads prometheus.yml at startup. Create the project directory and a config that scrapes Prometheus itself and the node_exporter you're about to run:

mkdir -p ~/prometheus && cd ~/prometheus
cat > prometheus.yml <<'YAML'
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    basic_auth:
      username: admin
      password_file: /etc/prometheus/admin-password
    static_configs:
      - targets: ["127.0.0.1:9090"]

  - job_name: node
    static_configs:
      - targets: ["127.0.0.1:9100"]
YAML

The basic_auth block on the prometheus job is there because the next step turns on authentication for Prometheus's own web server. Its self-scrape has to log in like any other client.

Generate the admin password and its bcrypt hash. Prometheus only accepts bcrypt hashes in its web config, never plaintext:

cd ~/prometheus
sudo apt-get install -y apache2-utils
[ -f admin-password ] || openssl rand -hex 16 > admin-password
chmod 644 admin-password
chmod 700 ~/prometheus
HASH=$(htpasswd -nbBC 10 "" "$(cat admin-password)" | tr -d ':\n')
printf 'basic_auth_users:\n  admin: %s\n' "$HASH" > web.yml
cat web.yml

The password is in ~/prometheus/admin-password. Read it with cat when you first log in. It is readable inside the container because Prometheus runs as the unprivileged nobody user and needs it for the self-scrape. The chmod 700 on the directory keeps other accounts on the server away from it.

Install Prometheus and node_exporter (Docker Compose)

Both containers use the host network. node_exporter requires it, since it has to see the host's real network interfaces and processes, as its README explains. Running Prometheus the same way lets it reach node_exporter on 127.0.0.1 with no published ports:

cd ~/prometheus
cat > docker-compose.yml <<'YAML'
services:
  prometheus:
    image: prom/prometheus:v3.15.0
    container_name: prometheus
    restart: unless-stopped
    network_mode: host
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=30d
      - --web.listen-address=127.0.0.1:9090
      - --web.config.file=/etc/prometheus/web.yml
      - --web.enable-lifecycle
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./web.yml:/etc/prometheus/web.yml:ro
      - ./admin-password:/etc/prometheus/admin-password:ro
      - prometheus-data:/prometheus

  node-exporter:
    image: quay.io/prometheus/node-exporter:v1.12.1
    container_name: node-exporter
    restart: unless-stopped
    network_mode: host
    pid: host
    command:
      - --path.rootfs=/host
      - --web.listen-address=127.0.0.1:9100
    volumes:
      - /:/host:ro,rslave

volumes:
  prometheus-data:
YAML

The flags are worth reading once:

  • command: replaces the image's defaults entirely. The image's default command is --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus, so both are repeated here. Leave out --storage.tsdb.path and the data ends up outside the volume, and a container recreate wipes it.
  • --storage.tsdb.retention.time=30d sets how long data is kept. Without it (and without --storage.tsdb.retention.size) the default is 15 days. Choose the number on purpose.
  • --web.enable-lifecycle lets you reload the config with an HTTP POST instead of a restart.
  • --path.rootfs=/host tells node_exporter that the host's filesystem is mounted at /host, so disk metrics describe the server and not the container.

Check the config with promtool (shipped in the same image) before you start anything. A typo here otherwise shows up as a crash loop. The --user flag runs the check as you, because the directory is now private to your account:

cd ~/prometheus
docker run --rm --user "$(id -u):$(id -g)" --entrypoint promtool \
  -v "$PWD":/etc/prometheus:ro prom/prometheus:v3.15.0 \
  check config /etc/prometheus/prometheus.yml

Start the stack and wait for Prometheus to report ready:

cd ~/prometheus
docker compose up -d
for i in $(seq 1 30); do
  curl -fs -u "admin:$(cat admin-password)" http://127.0.0.1:9090/-/ready && break
  sleep 2
done
docker compose ps

Confirm authentication is enforced and both targets are being scraped. The first curl has no credentials and should print 401:

cd ~/prometheus
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9090/api/v1/targets
sleep 20
curl -s -u "admin:$(cat admin-password)" http://127.0.0.1:9090/api/v1/targets \
  | grep -o '"health":"[a-z]*"'

Two "health":"up" lines mean both jobs are healthy. If node shows down, node_exporter isn't running. Check docker compose logs node-exporter.

HTTPS + domain

Point an A record for metrics.example.com at the server, then put Caddy in front of the loopback listener:

metrics.example.com {
    reverse_proxy 127.0.0.1:9090
}

Caddy installed on the host reaches 127.0.0.1:9090 directly. If you run Caddy as a container, 127.0.0.1 is the container's own loopback, so give that container network_mode: host as well.

The browser now asks for the admin user and the password from ~/prometheus/admin-password. Basic auth sits in Prometheus itself, not in Caddy, so it protects the API even for something on the box that bypasses the proxy.

Add a target and reload

Adding a target means editing prometheus.yml. For example, a second server running its own node_exporter:

  - job_name: node-web1
    static_configs:
      - targets: ["10.0.0.12:9100"]

Exporters on other machines have to be reachable from this box and should not be reachable from anywhere else. Use a private network or a WireGuard tunnel, or firewall port 9100 to this server's IP only. Then validate and reload without a restart:

cd ~/prometheus
docker run --rm --user "$(id -u):$(id -g)" --entrypoint promtool \
  -v "$PWD":/etc/prometheus:ro prom/prometheus:v3.15.0 \
  check config /etc/prometheus/prometheus.yml
curl -s -X POST -u "admin:$(cat admin-password)" http://127.0.0.1:9090/-/reload

Backups

The config files are small and precious. The TSDB is large and replaceable, since losing it costs you history and not the setup. Back up both, and stop the container for a consistent copy of the data:

cd ~/prometheus
tar czf prometheus-config-$(date +%F).tar.gz prometheus.yml web.yml docker-compose.yml
docker compose stop prometheus
docker run --rm -v prometheus_prometheus-data:/data -v "$PWD":/backup alpine \
  tar czf /backup/prometheus-data-$(date +%F).tar.gz -C /data .
docker compose start prometheus
ls -lh ~/prometheus/*.tar.gz

The volume name is prefixed with the compose project directory (prometheus_). Confirm it with docker volume ls. Copy the archives off the box. The password file is left out on purpose. Store it in your password manager instead.

Upgrades

Change the pinned tags in docker-compose.yml to the new releases, then:

cd ~/prometheus
docker compose pull
docker compose up -d

Read the release notes before a major version jump. Prometheus 3 changed the UI and some flag defaults compared with 2.x. The TSDB format is carried forward, but take a backup first.

Troubleshooting

The container restarts in a loop. Run docker compose logs prometheus. The usual causes are a YAML error (run promtool check config), a prometheus.yml that Docker created as a directory because the file didn't exist at first start (delete it, write the file, recreate the container), or a malformed web.yml.

Every request returns 401, even with the right password. The hash in web.yml must be bcrypt. Regenerate it with htpasswd -nbBC 10 as above. Plaintext and MD5 hashes are rejected.

The prometheus target is down with a 401. The self-scrape needs the basic_auth block in prometheus.yml. The password file must be mounted and readable by the nobody user inside the container.

Disk keeps growing. Check the retention flags and the series count (prometheus_tsdb_head_series in the query box). A label with unbounded values, such as user IDs or full URLs, is the classic cause.

Verification + next steps

You're done when https://metrics.example.com prompts for a password, the target health page (under Status) shows both jobs up, and the query node_memory_MemAvailable_bytes returns a value. Then restart the stack and confirm the history is still there.

Next: add Grafana and point a Prometheus data source at http://127.0.0.1:9090 with the same basic-auth credentials. For a lighter all-in-one host monitor, see Netdata. If you want logs and traces in the same place as your metrics, see SigNoz.

Next steps

How to self-host Prometheus →More self-hosted observability tools →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Pangolin on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.