Practical guide to set up Prometheus and Grafana with Docker Compose on a VPS

By · Published on · Updated on · 9 min read

Tablet on a desk showing graphs and dashboards, next to a phone and a coffee cup
Screen with charts and metrics representing infrastructure and container monitoring. Photo: AS Photography / Pexels

A hands‑on guide to set up Prometheus and Grafana with Docker Compose on a VPS, including Node Exporter, cAdvisor, Alertmanager, alert rules and datasource provisioning. By the end you'll have a working monitoring stack for hosts and containers, ready for reliable metrics, dashboards and alerts.

Before you start: prerequisites and stack architecture

You need a 64‑bit Linux VPS with root or sudo access, Docker and Docker Compose installed, required ports open and a user to operate the stack. Start with modest resources and scale up as metrics volume and retention grow.

The architecture includes Prometheus (collection/storage), Node Exporter (host metrics), cAdvisor (container metrics), Alertmanager (alert routing) and Grafana (dashboards and alerting).

If you still need to install Docker and Compose, follow this guide: install Docker and Docker Compose on Linux. Unsure about sizing? See how to choose the right VPS size.

Default components and ports inside the internal network:

Component Port Role Internal URL
Prometheus 9090 TSDB and scraping http://prometheus:9090
Alertmanager 9093 Groups and delivers alerts http://alertmanager:9093
Node Exporter 9100 Host metrics http://node-exporter:9100/metrics
cAdvisor 8080 Container metrics http://cadvisor:8080/metrics
Grafana 3000 Dashboards and alerts http://grafana:3000

Prepare directories, variables and permissions on the VPS

Create a project folder, separate configuration from persistent data and keep sensitive variables out of version control. Execute these steps as a sudo user.

We'll use a .env file for Grafana's initial password. Adjust names and paths according to your VPS policies.

  • Structure separates versionable configs from persistent volumes.
  • Use a password manager to store credentials.
  • Avoid weak passwords; enable MFA in Grafana after the first login.
# Crie a estrutura de diretórios
test -d monitoring || mkdir -p monitoring/{prometheus/rules,alertmanager,grafana/provisioning/{datasources,dashboards}}
cd monitoring

# Defina variáveis sensíveis (não comitar este arquivo)
cat > .env << 'EOF'
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=altere-esta-senha
EOF

# Opcional: restrinja permissões do .env
chmod 600 .env

Docker Compose: Prometheus, Grafana, Alertmanager, Node Exporter and cAdvisor

This docker-compose creates the network, defines persistent volumes and brings up all services. It enables Prometheus reload and Grafana provisioning.

Replace image tags according to your policy. You can manage containers via UI with Portainer if you prefer.

Following these steps lets you reproducibly set up Prometheus and Grafana with Docker Compose on a VPS.

  • Persistent volumes prevent data loss across recreations.
  • Pinned image tags reduce surprises from updates.
  • Grafana's password is read from .env so it doesn't leak into the repository.
version: "3.8"

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data:
  grafana_data:
  alertmanager_data:

services:
  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: prometheus
    restart: unless-stopped
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --web.enable-lifecycle
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules:/etc/prometheus/rules:ro
      - prometheus_data:/prometheus
    networks: [monitoring]

  alertmanager:
    image: prom/alertmanager:v0.27.0
    container_name: alertmanager
    restart: unless-stopped
    command: ["--config.file=/etc/alertmanager/alertmanager.yml"]
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - alertmanager_data:/alertmanager
    networks: [monitoring]

  node-exporter:
    image: prom/node-exporter:v1.7.0
    container_name: node-exporter
    restart: unless-stopped
    pid: host
    command: ["--path.rootfs=/host"]
    volumes:
      - /:/host:ro,rslave
    ports:
      - "9100:9100"
    networks: [monitoring]

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.47.2
    container_name: cadvisor
    restart: unless-stopped
    privileged: true
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks: [monitoring]

  grafana:
    image: grafana/grafana:10.4.3
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}
      - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    depends_on: [prometheus]
    networks: [monitoring]

Configure Prometheus: scrape_configs and rules

Prometheus will scrape itself, Node Exporter and cAdvisor. We also point to Alertmanager and load alert rules from the rules directory.

Adjust scrape_interval according to metrics volume and disk size. Shorter intervals increase CPU and storage usage.

  • Keep job names short and descriptive.
  • Centralize rules in separate files for organization.
  • We enable HTTP reload to apply changes without restarting.
global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]

rule_files:
  - /etc/prometheus/rules/*.yml

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["prometheus:9090"]

  - job_name: "node"
    static_configs:
      - targets: ["node-exporter:9100"]

  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]

Alertmanager: routes and alert delivery

Define how alerts are grouped, rate‑limited and delivered (email, webhook, chat). Start simple with SMTP. Replace placeholder values with valid credentials.

Test alert delivery with a deliberately sensitive rule, then tune thresholds to reduce noise.

  • Group by alertname and instance to avoid storms.
  • Adjust group_wait and repeat_interval according to criticality.
  • Use app passwords for SMTP when possible.
route:
  receiver: "email-default"
  group_by: ["alertname", "instance"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 3h

receivers:
  - name: "email-default"
    email_configs:
      - to: "[email protected]"
        from: "[email protected]"
        smarthost: "smtp.seu-dominio.com:587"
        auth_username: "[email protected]"
        auth_password: "SENHA-OU-TOKEN"
        require_tls: true

# Você pode adicionar outros receivers (webhook, chat) conforme necessidade.

Useful alerting rules to get started

Create a rules file with availability and saturation conditions. Avoid noisy alerts at first; refine after observing your environment.

The examples below cover instance down, high CPU, low memory and near‑full disk. Adjust thresholds to fit your reality.

  • Use labels for severity and the responsible team.
  • Add annotations with clear next steps.
  • Test expressions locally in Prometheus's expression browser.
groups:
  - name: infra-basics
    rules:
      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Instância fora do ar"
          description: "{{ $labels.instance }} não responde a scrape."

      - alert: NodeHighCpu
        expr: (1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))) > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU alta no host"
          description: "Uso de CPU acima de 80% por 5m em {{ $labels.instance }}."

      - alert: NodeLowMemory
        expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) < 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Memória disponível baixa"
          description: "Menos de 10% de memória livre em {{ $labels.instance }}."

      - alert: NodeDiskAlmostFull
        expr: (node_filesystem_avail_bytes{fstype=~"ext4|xfs",mountpoint!~"/(run|var/lib/docker.*)"} / node_filesystem_size_bytes{fstype=~"ext4|xfs",mountpoint!~"/(run|var/lib/docker.*)"}) < 0.1
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "Disco quase cheio"
          description: "Menos de 10% livre em {{ $labels.instance }} filesystem."

Provision Grafana: datasource and first dashboards

Provision the Prometheus datasource to avoid manual setup. Then log in to Grafana and import dashboards from the Import menu.

For containers, create panels for CPU, memory and restarts per service. For the host, add load, I/O and filesystem graphs. Start small and iterate.

  • A default datasource prevents URL mistakes.
  • Organize dashboards by domain: Infra, Docker, App.
  • Use dashboard variables to filter by instance.
# grafana/provisioning/datasources/datasource.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

Bring services up, check metrics and validate alerts

Now bring everything up and validate. These commands change host services; run them with care.

Open the local UIs to check: Prometheus on :9090, Alertmanager on :9093, Grafana on :3000. Test config reload and verify that rules are loaded.

With this you complete the end‑to‑end setup of Prometheus and Grafana with Docker Compose on a VPS and validate the monitoring flow.

  • Use docker compose logs to troubleshoot startup errors.
  • In Prometheus check Status > Targets and Status > Rules.
  • Trigger a temporary alert to validate delivery (e.g., lower thresholds).
# Subir em background
docker compose up -d

# Verificar status
docker compose ps

# Listar alvos no Prometheus (espera-se 200 OK)
curl -sS http://localhost:9090/targets | head -n 20

# Checar Alertmanager
curl -sS http://localhost:9093 | head -n 5

# Recarregar configuração do Prometheus após editar arquivos
echo "Reload Prometheus" && curl -X POST http://localhost:9090/-/reload

# Logs de um serviço específico
docker compose logs -f prometheus

Hardening, backups and next steps

To expose Grafana safely, put it behind a reverse proxy with TLS and authentication. The Let's Encrypt guide helps issue valid certificates.

Restrict external ports with a firewall; only open what's necessary. Back up volumes and your versioned configuration files regularly.

Once stable, consider separating metrics by environment and tuning retention according to available disk.

Frequently asked questions

What VPS size should I use for Prometheus and Grafana?

It depends on scrape_interval, number of targets and retention. For a few hosts and 15s intervals, modest CPU and memory are usually enough. Monitor host CPU, memory and I/O and scale vertically. As data grows, increase disk first, then CPU/RAM.

How do I persist data across container recreations?

Use named Docker volumes in Docker Compose for Prometheus, Grafana and Alertmanager. They live outside container lifecycle. Back up volumes and configuration files (.yml) regularly. Before major changes, create a VPS disk snapshot.

How do I secure Grafana in production?

Place Grafana behind a reverse proxy with TLS, restrict access via firewall, disable public sign-up, enable MFA and use strong passwords. If exposing to the internet, use valid certificates and keep Grafana updated. Review folder permissions and team access regularly.

Can I monitor multiple VPS instances with this setup?

Yes. Install Node Exporter on each VPS and add their IPs/ports to Prometheus's scrape_configs. For public exposure use firewall rules, TLS/ACLs or a secure tunnel. Exporters can also run as containers on each remote host.

Which notification channels can I use in Alertmanager?

Email via SMTP is the simplest starting point. You can add webhooks to integrate with chat or automation. Define routes by severity and service, group alerts to reduce noise and tune timeouts (group_wait, repeat_interval) based on criticality.

How do I update images without losing data?

Update tags in docker-compose.yml, back up volumes and run docker compose pull && docker compose up -d. Data persists in volumes. Validate updates in staging when possible and monitor logs and metrics after the upgrade.

Need this stack adapted to your environment?

If you want help reviewing rules, sizing retention or integrating alerts into your workflow, contact me to unlock the next step in your monitoring.

Talk about monitoring

See also