Skip to content
DevOps devops monitoring 4 min read

Monitoring Web Servers & Databases

Once your web server and databases are running, you need to watch them. Knowing your Nginx is up is not enough — you want to see how many requests it handles, whether your database is running out of connections, and if your cache is actually helping. The trick is that most services do not speak Prometheus’ language directly. We use small helper programs called exporters (a tiny program that reads metrics from one service and republishes them in a format Prometheus understands). This page wires Nginx and your databases into the Prometheus and Grafana stack you set up earlier.

How service monitoring fits together

Prometheus (a metrics database that “scrapes” — periodically pulls — numbers from targets) cannot read Nginx or PostgreSQL on its own. Each service needs a translator:

ServiceExporterWhat it reads
Nginxnginx-prometheus-exporterThe stub_status page
PostgreSQLpostgres_exporterSQL stats views
MySQL / MariaDBmysqld_exporterSHOW STATUS queries
Redisredis_exporterThe INFO command

Each exporter runs as a tiny HTTP server on its own port (commonly 9113, 9187, 9104, 9121). Prometheus scrapes those ports, stores the numbers, and Grafana (a dashboard tool that draws graphs from Prometheus data) turns them into charts.

When to use exporters: any time the thing you want to monitor is not itself a Prometheus-native app. Almost every database and web server falls into this category. When NOT to: if an app already exposes a /metrics endpoint (many Go and Java apps do), scrape it directly — no exporter needed.

Monitoring Nginx with stub_status

Nginx ships with a lightweight status page called stub_status (a built-in module that prints basic connection counts). First enable it.

sudo nano /etc/nginx/sites-available/status
server {
    listen 127.0.0.1:8080;
    server_name localhost;

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;   # only localhost may read it
        deny all;
    }
}

Enable the site and reload:

sudo ln -s /etc/nginx/sites-available/status /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl http://127.0.0.1:8080/nginx_status

Output:

Active connections: 3
server accepts handled requests
 142 142 318
Reading: 0 Writing: 1 Waiting: 2

Bind the status page to 127.0.0.1 and use allow/deny. Exposing stub_status publicly leaks traffic patterns to attackers.

Install the Nginx exporter

sudo useradd --no-create-home --shell /bin/false nginx_exporter
cd /tmp
curl -LO https://github.com/nginxinc/nginx-prometheus-exporter/releases/download/v1.4.0/nginx-prometheus-exporter_1.4.0_linux_amd64.tar.gz
tar xvf nginx-prometheus-exporter_1.4.0_linux_amd64.tar.gz
sudo mv nginx-prometheus-exporter /usr/local/bin/

Create a systemd service (systemd is Ubuntu’s service manager that starts and supervises background programs):

# /etc/systemd/system/nginx_exporter.service
[Unit]
Description=Nginx Prometheus Exporter
After=network.target

[Service]
User=nginx_exporter
ExecStart=/usr/local/bin/nginx-prometheus-exporter \
  --nginx.scrape-uri=http://127.0.0.1:8080/nginx_status
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now nginx_exporter
curl -s http://localhost:9113/metrics | grep nginx_connections

Output:

nginx_connections_active 3
nginx_connections_reading 0
nginx_connections_writing 1
nginx_connections_waiting 2

Metrics that matter: nginx_connections_active (current open connections — spikes mean load), nginx_http_requests_total (request rate — derive requests/second with rate()), and nginx_connections_waiting (idle keep-alive connections).

Monitoring PostgreSQL

The postgres_exporter logs into your database and reads its internal stats. Create a read-only monitoring user first.

sudo -u postgres psql
CREATE USER pg_exporter WITH PASSWORD 'StrongPass123';
GRANT pg_monitor TO pg_exporter;
\q

The pg_monitor role is a built-in PostgreSQL role that grants read access to all the stats views without giving any data access. Install the exporter:

cd /tmp
curl -LO https://github.com/prometheus-community/postgres_exporter/releases/download/v0.16.0/postgres_exporter-0.16.0.linux-amd64.tar.gz
tar xvf postgres_exporter-0.16.0.linux-amd64.tar.gz
sudo mv postgres_exporter-0.16.0.linux-amd64/postgres_exporter /usr/local/bin/
# /etc/systemd/system/postgres_exporter.service
[Unit]
Description=Postgres Exporter
After=network.target

[Service]
Environment=DATA_SOURCE_NAME=postgresql://pg_exporter:StrongPass123@localhost:5432/postgres?sslmode=disable
ExecStart=/usr/local/bin/postgres_exporter
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now postgres_exporter
curl -s http://localhost:9187/metrics | grep pg_stat_database_blks

Metrics that matter: pg_stat_database_numbackends (active connections — watch it against max_connections), pg_stat_database_xact_commit (commit rate = how busy the DB is), and the cache hit ratio from pg_stat_database_blks_hit vs blks_read (you want hits well above 99%).

MySQL and Redis exporters

For MySQL/MariaDB, create a limited user and run mysqld_exporter:

CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'StrongPass123';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';

Point it with DATA_SOURCE_NAME='exporter:StrongPass123@(localhost:3306)/'. Key metrics: mysql_global_status_threads_connected, mysql_global_status_queries, and mysql_global_status_slow_queries.

For Redis, the exporter just needs the address:

redis_exporter -redis.addr redis://localhost:6379

Key Redis metrics: redis_connected_clients, redis_keyspace_hits_total vs redis_keyspace_misses_total (your cache hit ratio), and redis_memory_used_bytes.

Wire the exporters into Prometheus

Add the targets to your Prometheus config so it scrapes each port:

# /etc/prometheus/prometheus.yml
scrape_configs:
  - job_name: "nginx"
    static_configs:
      - targets: ["localhost:9113"]
  - job_name: "postgres"
    static_configs:
      - targets: ["localhost:9187"]
  - job_name: "mysql"
    static_configs:
      - targets: ["localhost:9104"]
  - job_name: "redis"
    static_configs:
      - targets: ["localhost:9121"]
sudo systemctl restart prometheus

Open Prometheus at http://your-server:9090, go to Status > Targets, and confirm every job shows UP. Then in Grafana, import community dashboards by ID: 12708 (Nginx), 9628 (PostgreSQL), 7362 (MySQL), and 11835 (Redis). They render the metrics above instantly.

Best Practices

  • Give every exporter a dedicated read-only user — never reuse your application or root/postgres superuser credentials.
  • Bind exporters and stub_status to 127.0.0.1 and block their ports with ufw (Ubuntu’s firewall) so they are not reachable from the internet.
  • Track ratios, not raw counters: connections-used vs max, cache hits vs misses. Ratios reveal trouble before a hard failure.
  • Run each exporter under systemd with Restart=always so it comes back after a crash or reboot.
  • Pin exporter versions in your install scripts so upgrades are deliberate and reproducible.
  • Set alerts on connection saturation and cache-hit drops — these predict outages earlier than CPU graphs do.
Last updated June 15, 2026
Was this helpful?