We earn commissions when you shop through the links below.
If you’re running your own servers, knowing how to set up monitoring for self-hosted apps is non-negotiable. Without visibility into uptime, resource usage, and error rates, you’re flying blind — and you’ll usually find out something broke from a user, not an alert. In this guide, I’ll walk through a practical monitoring stack using Prometheus, Grafana, and Uptime Kuma that you can deploy on any VPS or dedicated server in under an hour.
Why Monitoring Matters for Self-Hosted Apps
Managed platforms like Vercel or Railway handle a lot of operational concerns for you. But the moment you self-host — whether for cost, control, or compliance — you own the reliability. That means tracking:
- Uptime — Is the app responding to HTTP requests?
- System resources — CPU, memory, disk usage on the host
- Application metrics — Request rates, error rates, response times
- Logs — Structured output you can query when things go wrong
The good news is that the open-source ecosystem here is excellent. You don’t need to pay for Datadog or New Relic to get solid observability.
The Stack I Recommend
Here’s what I use and what this guide covers:
- Uptime Kuma — Lightweight HTTP/TCP uptime monitoring with a clean UI and built-in alerting
- Prometheus — Metrics collection and storage
- Node Exporter — Exposes host-level metrics to Prometheus
- Grafana — Dashboards and alerting on top of Prometheus
I’ll assume you’re running on a Linux VPS. If you need a solid host, DigitalOcean Droplets are my go-to — cheap, reliable, and easy to snapshot before experiments like this one.
Step 1: Install Uptime Kuma
Uptime Kuma is the fastest win. It runs as a single Docker container and gives you HTTP/HTTPS monitoring, status pages, and notifications to Slack, Telegram, email, and more.
# Create a directory for Uptime Kuma data
mkdir -p ~/monitoring/uptime-kuma
# Run Uptime Kuma with Docker
docker run -d \
--name uptime-kuma \
--restart=always \
-p 3001:3001 \
-v ~/monitoring/uptime-kuma:/app/data \
louislam/uptime-kuma:1
Once it’s running, hit http://your-server-ip:3001, create an admin account, and start adding monitors. For each self-hosted app, add an HTTP(s) monitor pointing at its public URL or internal health endpoint. Set the check interval to 60 seconds and configure at least one notification channel.
I’d recommend putting Uptime Kuma behind Nginx with a TLS cert so you can access it securely and expose a public status page to your users.
Step 2: Deploy Prometheus and Node Exporter
Uptime Kuma tells you if your app is up. Prometheus tells you why it might be struggling. The combination of Prometheus + Node Exporter gives you host-level CPU, memory, disk I/O, and network metrics without touching your application code.
I use Docker Compose for this because it keeps everything in one place:
# ~/monitoring/docker-compose.yml
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: always
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=15d"
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: always
network_mode: host
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=changeme
volumes:
prometheus_data:
grafana_data:
Now create the Prometheus config file:
# ~/monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["localhost:9100"]
# Add your app's metrics endpoint if it exposes one
- job_name: "my-app"
static_configs:
- targets: ["localhost:8080"]
metrics_path: /metrics
Start the stack:
cd ~/monitoring
docker compose up -d
Prometheus is now scraping Node Exporter every 15 seconds. You can verify at http://your-server-ip:9090/targets.
Step 3: Configure Grafana Dashboards
Grafana connects to Prometheus as a data source and gives you visual dashboards. Head to http://your-server-ip:3000, log in with admin/changeme (change this immediately), then:
- Go to Connections → Data Sources → Add data source
- Select Prometheus, set URL to
http://prometheus:9090, save and test - Go to Dashboards → Import
- Enter dashboard ID
1860(Node Exporter Full — one of the most popular community dashboards) - Select your Prometheus data source and import
You now have a full host metrics dashboard showing CPU, memory, disk, and network in real time.
Step 4: Set Up Alerting
Dashboards are great for debugging. Alerts are what actually save you at 2am. Here’s a simple Grafana alert rule for high memory usage:
In Grafana, go to Alerting → Alert Rules → New alert rule. Use this PromQL expression:
(
node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes
) / node_memory_MemTotal_bytes * 100
Set the condition to fire when this value is above 90 for 5 minutes. Configure a notification channel (Grafana supports Slack, PagerDuty, email, webhooks, and more) under Alerting → Contact points.
For Uptime Kuma, go into each monitor’s settings and add a notification. I pipe mine into a dedicated Slack channel so downtime alerts are separate from other noise.
Step 5: Expose App-Level Metrics
Host metrics tell you about the machine. App metrics tell you about your code. If you’re running a Node.js app, adding Prometheus metrics takes about five minutes:
npm install prom-client
// metrics.js
const client = require('prom-client');
const register = new client.Registry();
client.collectDefaultMetrics({ register });
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
registers: [register],
});
// Express middleware
function metricsMiddleware(req, res, next) {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode,
});
});
next();
}
// Expose /metrics endpoint
async function metricsHandler(req, res) {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
}
module.exports = { metricsMiddleware, metricsHandler };
Wire it into your Express app, then add the endpoint to your prometheus.yml scrape config. Now Grafana can show you p95 response times per route, error rates, and throughput — exactly what you need to diagnose slow endpoints.
For PHP and Laravel apps, check out spatie/laravel-prometheus — it integrates cleanly with the same Prometheus setup.
Keeping It Secure
A few things before you call this production-ready:
- Don’t expose Prometheus or Node Exporter publicly. Bind them to localhost or an internal network only. Only Grafana and Uptime Kuma need public access.
- Put Grafana behind a reverse proxy with TLS. Use Nginx + Certbot or Caddy.
- Change all default passwords. Grafana’s admin password, at minimum.
- Set data retention limits. The
--storage.tsdb.retention.time=15dflag in the Compose file prevents Prometheus from eating your disk.
If your self-hosted apps live on a shared host rather than a VPS, some of these tools won’t be available to you. In that case, look at Hostinger‘s VPS plans — they’re affordable and give you full root access to run Docker and this entire stack.
Final Thoughts
Knowing how to set up monitoring for self-hosted apps is one of those skills that pays for itself the first time it catches an outage before your users do. The stack I’ve described — Uptime Kuma for uptime checks, Prometheus + Node Exporter for host metrics, and Grafana for dashboards and alerts — covers the vast majority of what you need without any ongoing SaaS costs.
Start with Uptime Kuma today if you haven’t already. It takes ten minutes and immediately gives you peace of mind. Then layer in Prometheus and Grafana when you’re ready to go deeper on performance and resource trends. Once you have this running, you’ll wonder how you ever shipped without it.