Monitoring and Observability Guide¶
Current State Analysis (Updated 2025-01-02)¶
Monitoring Stack Status: ✅ OPERATIONAL¶
The monitoring stack has been successfully deployed and is fully operational with the following components:
- ✅ Centralized Metrics Collection - Prometheus collecting from all exporters
- ✅ Log Aggregation - Loki collecting and indexing container logs
- ✅ Alerting System - AlertManager routing alerts to n8n workflows
- ✅ Comprehensive Health Checks - Blackbox exporter monitoring all services
- ✅ Performance Metrics - Complete system and container resource tracking
- ✅ Application Metrics - HTTP metrics, database stats, backup monitoring
- ✅ Visualization - Grafana dashboards for all major services and infrastructure
Recent Fixes Applied¶
Permission Issues Resolved:
- Created /srv/dockerdata/monitoring/fix-permissions.sh script
- Fixed ownership for Prometheus (user 65534), Grafana (user 472), Loki (user 10001), AlertManager (user 65534)
- Disabled Watchtower auto-updates for monitoring stack to prevent permission issues
AlertManager Configuration Stabilized: - Resolved "time interval crossing midnight" errors - Simplified configuration with basic webhook routing to n8n - Removed complex time-based routing that was causing startup failures - Maintained full functionality through n8n workflow integration
Monitoring Stack Stability: - All services running consistently without restarts - Metrics collection stable across all exporters - Alert delivery functioning through n8n webhook integration - Dashboard performance optimized
Permission Fix Script¶
A critical fix has been implemented to address permission issues that were preventing the monitoring stack from starting properly.
Script Location¶
Usage¶
# Run the script if monitoring services fail to start with permission errors
/srv/dockerdata/monitoring/fix-permissions.sh
What It Fixes¶
The script corrects ownership and permissions for all monitoring service data directories:
- Prometheus: Sets ownership to user 65534 (nobody)
- Grafana: Sets ownership to user 472 (grafana)
- Loki: Sets ownership to user 10001 (loki)
- AlertManager: Sets ownership to user 65534 (nobody)
When to Use¶
- After system updates that might change Docker volume permissions
- When adding the monitoring stack to a new environment
- If any monitoring service fails to start with "permission denied" errors
- After manually manipulating files in monitoring data directories
Prevention Measures¶
- Watchtower Exclusion: All monitoring containers have
com.centurylinklabs.watchtower.enable=falselabels - Regular Backups: Permission fix script is included in infrastructure backups
- Documentation: Troubleshooting steps documented in service-specific guides
Implementing Prometheus + Grafana Stack¶
1. Create Monitoring Stack¶
# Create monitoring directory
mkdir -p /srv/dockerdata/monitoring/{prometheus,grafana,alertmanager}
# Create docker-compose.yml
cat > /srv/dockerdata/monitoring/docker-compose.yml << 'EOF'
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
networks:
- traefik_proxy
- monitoring
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_proxy"
- "traefik.http.routers.prometheus.rule=Host(`prometheus.rbnk.uk`)"
- "traefik.http.routers.prometheus.entrypoints=websecure"
- "traefik.http.routers.prometheus.tls=true"
- "traefik.http.routers.prometheus.tls.certresolver=cloudflare"
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
- "traefik.http.routers.prometheus.middlewares=auth@file"
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://grafana.rbnk.uk
- GF_SMTP_ENABLED=true
- GF_SMTP_HOST=${SMTP_HOST:-smtp.gmail.com:587}
- GF_SMTP_USER=${SMTP_USER}
- GF_SMTP_PASSWORD=${SMTP_PASSWORD}
- GF_SMTP_FROM_ADDRESS=${SMTP_FROM}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- traefik_proxy
- monitoring
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_proxy"
- "traefik.http.routers.grafana.rule=Host(`grafana.rbnk.uk`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls=true"
- "traefik.http.routers.grafana.tls.certresolver=cloudflare"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
restart: unless-stopped
volumes:
- ./alertmanager/config.yml:/etc/alertmanager/config.yml
- alertmanager_data:/alertmanager
networks:
- monitoring
command:
- '--config.file=/etc/alertmanager/config.yml'
- '--storage.path=/alertmanager'
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
networks:
- monitoring
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
devices:
- /dev/kmsg
networks:
- monitoring
postgres-exporter:
image: prometheuscommunity/postgres-exporter:latest
container_name: postgres-exporter
restart: unless-stopped
environment:
DATA_SOURCE_NAME: "postgresql://postgres:${POSTGRES_PASSWORD}@supa-db:5432/postgres?sslmode=disable"
networks:
- monitoring
- supabase_default
volumes:
prometheus_data:
grafana_data:
alertmanager_data:
networks:
monitoring:
driver: bridge
traefik_proxy:
external: true
supabase_default:
external: true
EOF
# Create .env file
cat > /srv/dockerdata/monitoring/.env << 'EOF'
GRAFANA_PASSWORD=changeme
POSTGRES_PASSWORD=your-supabase-postgres-password
SMTP_HOST=smtp.gmail.com:587
[email protected]
SMTP_PASSWORD=your-app-password
[email protected]
EOF
chmod 640 /srv/dockerdata/monitoring/.env
2. Configure Prometheus¶
# Create Prometheus configuration
cat > /srv/dockerdata/monitoring/prometheus/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'docker-monitoring'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
rule_files:
- "alerts.yml"
scrape_configs:
# Docker host metrics
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
# Container metrics
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
# PostgreSQL metrics
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
# Traefik metrics
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Docker daemon metrics
- job_name: 'docker'
static_configs:
- targets: ['172.17.0.1:9323']
EOF
3. Create Alert Rules¶
cat > /srv/dockerdata/monitoring/prometheus/alerts.yml << 'EOF'
groups:
- name: system
rules:
- alert: HighCPUUsage
expr: (100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is above 80% (current value: {{ $value }}%)"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage detected"
description: "Memory usage is above 85% (current value: {{ $value }}%)"
- alert: DiskSpaceLow
expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 15
for: 5m
labels:
severity: critical
annotations:
summary: "Low disk space"
description: "Disk space is below 15% (current value: {{ $value }}%)"
- name: containers
rules:
- alert: ContainerDown
expr: up{job="cadvisor"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Container is down"
description: "Container {{ $labels.name }} has been down for more than 1 minute"
- alert: ContainerHighCPU
expr: (rate(container_cpu_usage_seconds_total[5m]) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "Container high CPU usage"
description: "Container {{ $labels.name }} CPU usage is above 80%"
- alert: ContainerHighMemory
expr: (container_memory_usage_bytes / container_spec_memory_limit_bytes) * 100 > 90
for: 5m
labels:
severity: warning
annotations:
summary: "Container high memory usage"
description: "Container {{ $labels.name }} memory usage is above 90%"
- name: postgres
rules:
- alert: PostgresDown
expr: pg_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "PostgreSQL is down"
description: "PostgreSQL instance is down"
- alert: PostgresHighConnections
expr: pg_stat_database_numbackends / pg_settings_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL high connection count"
description: "PostgreSQL connection count is above 80% of max_connections"
- alert: PostgresDeadlocks
expr: increase(pg_stat_database_deadlocks[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL deadlocks detected"
description: "PostgreSQL has {{ $value }} deadlocks in the last 5 minutes"
- name: backup
rules:
- alert: BackupFailed
expr: backup_last_success_timestamp < (time() - 86400)
for: 1h
labels:
severity: critical
annotations:
summary: "Backup job failed"
description: "Last successful backup was more than 24 hours ago"
- name: gitea
rules:
- alert: GiteaDown
expr: up{job="gitea"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Gitea is down"
description: "Gitea service is not responding"
- alert: GiteaSyncFailed
expr: gitea_sync_last_success < (time() - 3600)
for: 30m
labels:
severity: warning
annotations:
summary: "Gitea sync job failed"
description: "Last successful sync to GitHub was more than 1 hour ago"
- alert: GiteaHighDiskUsage
expr: (gitea_repository_size_bytes / gitea_disk_quota_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Gitea high disk usage"
description: "Gitea repositories using > 90% of allocated space"
EOF
4. Configure Alertmanager¶
cat > /srv/dockerdata/monitoring/alertmanager/config.yml << 'EOF'
global:
resolve_timeout: 5m
# Using Mailrise SMTP gateway for email-to-notification conversion
smtp_from: '[email protected]'
smtp_smarthost: 'mailrise:8025'
smtp_require_tls: false
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 10s
group_interval: 10s
repeat_interval: 12h
receiver: 'default'
routes:
- match:
severity: critical
receiver: 'critical'
repeat_interval: 1h
- match:
severity: warning
receiver: 'warning'
receivers:
# Default receiver - uses Apprise for multi-channel notifications
- name: 'default'
webhook_configs:
- url: 'http://apprise:8000/notify/apprise'
send_resolved: true
# Critical alerts - high priority with multiple channels
- name: 'critical'
webhook_configs:
- url: 'http://apprise:8000/notify/apprise?tag=critical,oncall'
send_resolved: true
email_configs:
- to: '[email protected]'
headers:
Subject: '[CRITICAL] {{ .GroupLabels.alertname }}'
# Warning alerts - normal priority
- name: 'warning'
webhook_configs:
- url: 'http://apprise:8000/notify/apprise?tag=warning'
send_resolved: true
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']
EOF
# Note: The notification stack integration provides:
# - Apprise: Routes to 100+ notification services (WhatsApp, Telegram, Discord, etc.)
# - ntfy: Push notifications to mobile/desktop apps
# - Mailrise: Converts AlertManager emails to push notifications
# - Uptime Kuma: Additional service monitoring with its own notifications
# Configure actual notification URLs in Apprise web UI at https://apprise.rbnk.uk
5. Enable Docker Metrics¶
# Configure Docker daemon for metrics
sudo tee /etc/docker/daemon.json > /dev/null << 'EOF'
{
"metrics-addr": "172.17.0.1:9323",
"experimental": true
}
EOF
# Restart Docker
sudo systemctl restart docker
6. Add Traefik Metrics¶
# Update Traefik configuration
cat >> /srv/dockerdata/traefik/traefik.yml << 'EOF'
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
EOF
# Restart Traefik
cd /srv/dockerdata/traefik
docker compose restart
Grafana Dashboard Setup¶
1. Provision Datasources¶
mkdir -p /srv/dockerdata/monitoring/grafana/provisioning/datasources
cat > /srv/dockerdata/monitoring/grafana/provisioning/datasources/prometheus.yml << 'EOF'
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
EOF
2. Import Dashboards¶
mkdir -p /srv/dockerdata/monitoring/grafana/provisioning/dashboards
# Dashboard provisioning config
cat > /srv/dockerdata/monitoring/grafana/provisioning/dashboards/dashboards.yml << 'EOF'
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
EOF
# Download pre-built dashboards
cd /srv/dockerdata/monitoring/grafana/provisioning/dashboards
# Node Exporter Dashboard
wget https://grafana.com/api/dashboards/1860/revisions/latest/download -O node-exporter.json
# Docker Dashboard
wget https://grafana.com/api/dashboards/893/revisions/latest/download -O docker.json
# PostgreSQL Dashboard
wget https://grafana.com/api/dashboards/9628/revisions/latest/download -O postgresql.json
# Traefik Dashboard
wget https://grafana.com/api/dashboards/12250/revisions/latest/download -O traefik.json
Log Aggregation with Loki¶
1. Add Loki to Monitoring Stack¶
# Update docker-compose.yml
cat >> /srv/dockerdata/monitoring/docker-compose.yml << 'EOF'
loki:
image: grafana/loki:latest
container_name: loki
restart: unless-stopped
volumes:
- ./loki/config.yml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring
promtail:
image: grafana/promtail:latest
container_name: promtail
restart: unless-stopped
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail/config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
networks:
- monitoring
volumes:
loki_data:
EOF
2. Configure Loki¶
mkdir -p /srv/dockerdata/monitoring/loki
cat > /srv/dockerdata/monitoring/loki/config.yml << 'EOF'
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://alertmanager:9093
analytics:
reporting_enabled: false
EOF
3. Configure Promtail¶
mkdir -p /srv/dockerdata/monitoring/promtail
cat > /srv/dockerdata/monitoring/promtail/config.yml << 'EOF'
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: containers
static_configs:
- targets:
- localhost
labels:
job: containerlogs
__path__: /var/lib/docker/containers/*/*log
pipeline_stages:
- json:
expressions:
output: log
stream: stream
attrs:
- json:
expressions:
tag:
source: attrs
- regex:
expression: (?P<container_name>(?:[^|]*))\|(?P<image_name>(?:[^|]*))
source: tag
- timestamp:
format: RFC3339Nano
source: time
- labels:
stream:
container_name:
image_name:
- output:
source: output
- job_name: syslog
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
EOF
Application-Specific Monitoring¶
Gitea Metrics¶
# Add Gitea monitoring script
cat > /srv/dockerdata/_scripts/gitea-metrics.sh << 'EOF'
#!/bin/bash
# Gitea metrics exporter for Prometheus
# Repository count
echo "# HELP gitea_repositories_total Total number of repositories"
echo "# TYPE gitea_repositories_total gauge"
docker exec gitea sqlite3 /data/gitea/gitea.db \
"SELECT COUNT(*) FROM repository;" | \
xargs echo "gitea_repositories_total"
# User count
echo "# HELP gitea_users_total Total number of users"
echo "# TYPE gitea_users_total gauge"
docker exec gitea sqlite3 /data/gitea/gitea.db \
"SELECT COUNT(*) FROM user WHERE type = 0;" | \
xargs echo "gitea_users_total"
# Repository sizes
echo "# HELP gitea_repository_size_bytes Repository size in bytes"
echo "# TYPE gitea_repository_size_bytes gauge"
docker exec gitea find /data/git/repositories -maxdepth 2 -mindepth 2 -type d | \
while read repo; do
size=$(docker exec gitea du -sb "$repo" | awk '{print $1}')
name=$(basename "$(dirname "$repo")")/$(basename "$repo")
echo "gitea_repository_size_bytes{repo=\"$name\"} $size"
done
# Last sync status
echo "# HELP gitea_sync_last_success Last successful sync timestamp"
echo "# TYPE gitea_sync_last_success gauge"
if [[ -f /var/log/gitea-sync.log ]]; then
last_success=$(grep "Sync completed successfully" /var/log/gitea-sync.log | \
tail -1 | awk '{print $1 " " $2}')
if [[ -n "$last_success" ]]; then
timestamp=$(date -d "$last_success" +%s)
echo "gitea_sync_last_success $timestamp"
fi
fi
EOF
chmod +x /srv/dockerdata/_scripts/gitea-metrics.sh
Gitea Sync Monitoring¶
# Monitor sync jobs
cat > /srv/dockerdata/_scripts/monitor-gitea-sync.sh << 'EOF'
#!/bin/bash
# Monitor Gitea to GitHub sync status
LOG_FILE="/var/log/gitea-sync.log"
WEBHOOK_URL="${DISCORD_WEBHOOK_URL}"
MAX_AGE_MINUTES=30
# Check last sync time
if [[ -f "$LOG_FILE" ]]; then
last_sync=$(grep "Sync completed" "$LOG_FILE" | tail -1)
if [[ -n "$last_sync" ]]; then
last_time=$(echo "$last_sync" | awk '{print $1 " " $2}')
age_minutes=$(( ($(date +%s) - $(date -d "$last_time" +%s)) / 60 ))
if [[ $age_minutes -gt $MAX_AGE_MINUTES ]]; then
message="⚠️ Gitea sync hasn't run in $age_minutes minutes"
curl -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
"$WEBHOOK_URL"
fi
fi
fi
# Check for sync errors
recent_errors=$(tail -100 "$LOG_FILE" | grep -c "ERROR")
if [[ $recent_errors -gt 0 ]]; then
message="🚨 Found $recent_errors errors in recent Gitea sync logs"
curl -H "Content-Type: application/json" \
-d "{\"content\": \"$message\"}" \
"$WEBHOOK_URL"
fi
EOF
chmod +x /srv/dockerdata/_scripts/monitor-gitea-sync.sh
# Add to cron
echo "*/15 * * * * /srv/dockerdata/_scripts/monitor-gitea-sync.sh" | crontab -
Supabase Metrics¶
# Add custom metrics endpoint to Supabase
cat > /srv/dockerdata/supabase/metrics-exporter.sh << 'EOF'
#!/bin/bash
# Custom Supabase metrics exporter
# Database stats
echo "# HELP supabase_database_size_bytes Database size in bytes"
echo "# TYPE supabase_database_size_bytes gauge"
docker exec supa-db psql -U postgres -t -c \
"SELECT pg_database_size('postgres');" | \
xargs echo "supabase_database_size_bytes"
# Connection stats
echo "# HELP supabase_connections Active database connections"
echo "# TYPE supabase_connections gauge"
docker exec supa-db psql -U postgres -t -c \
"SELECT count(*) FROM pg_stat_activity;" | \
xargs echo "supabase_connections"
# Table sizes
echo "# HELP supabase_table_size_bytes Table size in bytes"
echo "# TYPE supabase_table_size_bytes gauge"
docker exec supa-db psql -U postgres -t -c \
"SELECT schemaname, tablename, pg_total_relation_size(schemaname||'.'||tablename)
FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema');" | \
while read schema table size; do
echo "supabase_table_size_bytes{schema=\"$schema\",table=\"$table\"} $size"
done
EOF
chmod +x /srv/dockerdata/supabase/metrics-exporter.sh
Health Check Implementation¶
1. Service Health Endpoints¶
# Create health check service
cat > /srv/dockerdata/monitoring/health-checker/docker-compose.yml << 'EOF'
version: '3.8'
services:
health-checker:
image: willfarrell/ping:latest
container_name: health-checker
restart: unless-stopped
environment:
CHECKS: |
{
"supabase-api": {
"url": "https://supabase.rbnk.uk/rest/v1/",
"method": "GET",
"expectedStatusCode": 200,
"timeout": 10
},
"supabase-auth": {
"url": "https://supabase.rbnk.uk/auth/v1/health",
"method": "GET",
"expectedStatusCode": 200,
"timeout": 10
},
"traefik": {
"url": "http://traefik:8080/ping",
"method": "GET",
"expectedStatusCode": 200,
"timeout": 5
},
"grafana": {
"url": "https://grafana.rbnk.uk/api/health",
"method": "GET",
"expectedStatusCode": 200,
"timeout": 10
},
"gitea": {
"url": "https://gitea.rbnk.uk/api/v1/version",
"method": "GET",
"expectedStatusCode": 200,
"timeout": 10
}
}
networks:
- monitoring
- traefik_proxy
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9090"
networks:
monitoring:
external: true
traefik_proxy:
external: true
EOF
2. Custom Health Checks¶
# Add to each service's docker-compose.yml
cat >> /srv/dockerdata/supabase/docker-compose.yml << 'EOF'
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 5s
retries: 5
EOF
Resource Usage Tracking¶
1. Create Resource Dashboard¶
{
"dashboard": {
"title": "Resource Usage",
"panels": [
{
"title": "CPU Usage by Container",
"targets": [{
"expr": "rate(container_cpu_usage_seconds_total[5m]) * 100"
}]
},
{
"title": "Memory Usage by Container",
"targets": [{
"expr": "container_memory_usage_bytes / 1024 / 1024 / 1024"
}]
},
{
"title": "Network I/O",
"targets": [{
"expr": "rate(container_network_receive_bytes_total[5m])"
}]
},
{
"title": "Disk I/O",
"targets": [{
"expr": "rate(container_fs_reads_bytes_total[5m])"
}]
}
]
}
}
Notification Stack Integration¶
Connecting Monitoring to Notifications¶
The monitoring stack integrates seamlessly with the notification system:
- AlertManager → Apprise
- All alerts route through Apprise for multi-channel delivery
- Tags control which services receive which alerts
-
Supports 100+ notification services
-
Uptime Kuma Integration
- Provides additional service monitoring
- Has its own notification configuration
-
Can send to Apprise for unified notifications
-
Direct ntfy Integration
- Critical alerts can bypass Apprise and go directly to ntfy
- Useful for high-priority mobile push notifications
- Topic structure:
monitoring-severity-category
Setting Up Notification Channels¶
# 1. Configure Apprise (via web UI at https://apprise.rbnk.uk)
# Add notification URLs for your preferred services:
# - WhatsApp: whatsapp://phone@number
# - Telegram: tgram://bottoken/ChatID
# - Discord: discord://webhook_id/webhook_token
# - Email: mailto://user:[email protected]
# 2. Configure ntfy authentication
# Service accounts are pre-configured for monitoring:
# Username: monitoring
# Password: (check /srv/dockerdata/ntfy/.env)
# 3. Test notifications
curl -X POST http://apprise:8000/notify/apprise \
-H "Content-Type: application/json" \
-d '{"title": "Test Alert", "body": "Monitoring test", "tag": "test"}'
Monitoring Best Practices¶
Key Metrics to Monitor¶
- System Metrics
- CPU usage (< 80%)
- Memory usage (< 85%)
- Disk usage (< 85%)
- Network I/O
-
System load
-
Container Metrics
- Container status
- Resource usage per container
- Container restarts
-
Exit codes
-
Application Metrics
- Request rate
- Error rate
- Response time (p50, p95, p99)
- Queue depth
-
Database connections
-
Business Metrics
- Active users
- Transaction volume
- API usage
- Storage consumption
Alert Configuration¶
# Severity Levels
- critical: Immediate action required (pager)
- warning: Investigation needed (email)
- info: Informational only (log)
# Alert Routing
- Database down: critical
- High resource usage: warning
- Backup failure: critical
- Certificate expiry: warning
- Service degradation: warning
Maintenance Tasks¶
- Daily
- Check dashboard for anomalies
- Review recent alerts
-
Verify backup completion
-
Weekly
- Review resource trends
- Check for security alerts
-
Update dashboards as needed
-
Monthly
- Review and tune alerts
- Analyze performance trends
- Capacity planning
- Update monitoring documentation
Troubleshooting¶
Common Issues¶
-
Prometheus not scraping
-
Grafana datasource issues
-
Missing metrics
Service-Specific Monitoring¶
Rclone Backup Monitoring¶
Monitor cloud backup operations for reliability and performance:
- Metrics Collection
- Textfile Collector: Rclone metrics are collected via the Node Exporter textfile collector
- Script Location:
/srv/dockerdata/rclone/collect-metrics.sh - Schedule: Runs every 5 minutes via cron
-
Metrics Endpoint: Exposed through node-exporter at port 9100
-
Backup Monitoring
The monitoring system tracks both infrastructure and cloud backups:
Infrastructure Backup Metrics:
- rclone_infrastructure_backup_last_success_timestamp - Unix timestamp of last successful backup
- rclone_infrastructure_backup_errors_24h - Number of backup errors in the last 24 hours
Cloud Backup Metrics:
- rclone_cloud_backup_last_success_timestamp - Unix timestamp of last successful cloud sync
- rclone_cloud_backup_errors_24h - Number of sync errors in the last 24 hours
- rclone_cloud_backup_size_bytes{provider="gdrive|dropbox|onedrive"} - Size of backups per cloud provider
R2 Storage Metrics:
- rclone_r2_used_bytes - Used storage in Cloudflare R2
- rclone_r2_total_bytes - Total available storage (1TB limit)
- rclone_r2_usage_percent - Percentage of R2 storage used
Collection Metrics:
- rclone_metrics_collection_timestamp - Unix timestamp of last metrics collection (for freshness check)
- Grafana Dashboards
Rclone Backup Monitoring Dashboard (Located in Infrastructure folder): - Auto-refresh: Every 30 seconds - Key Panels: - Backup Status Overview - Shows last backup times and current status - R2 Storage Usage Gauge - Visual representation of storage utilization - Storage Distribution by Provider - Pie chart showing backup sizes across cloud providers - Storage Growth Trends - Time series showing storage usage over time - Error Count Tracking - Displays backup errors over the last 24 hours
- Alert Rules
The following Rclone-specific alerts are configured:
- alert: InfrastructureBackupMissing
expr: time() - rclone_infrastructure_backup_last_success_timestamp > 172800
for: 5m
labels:
severity: critical
annotations:
summary: "Infrastructure backup missing for 48 hours"
description: "Last successful infrastructure backup was {{ $value | humanizeDuration }} ago"
- alert: CloudBackupMissing
expr: time() - rclone_cloud_backup_last_success_timestamp > 172800
for: 5m
labels:
severity: warning
annotations:
summary: "Cloud backup sync missing for 48 hours"
description: "Last successful cloud sync was {{ $value | humanizeDuration }} ago"
- alert: BackupErrors
expr: (rclone_infrastructure_backup_errors_24h > 0) or (rclone_cloud_backup_errors_24h > 0)
for: 5m
labels:
severity: warning
annotations:
summary: "Backup errors detected"
description: "{{ $value }} backup errors in the last 24 hours"
- alert: R2StorageHigh
expr: rclone_r2_usage_percent > 80
for: 15m
labels:
severity: warning
annotations:
summary: "R2 storage usage high"
description: "R2 storage is {{ $value }}% full"
- alert: RcloneMetricsStale
expr: time() - rclone_metrics_collection_timestamp > 600
for: 5m
labels:
severity: warning
annotations:
summary: "Rclone metrics collection stale"
description: "Metrics haven't been collected for {{ $value | humanizeDuration }}"
- Example Queries
# Time since last infrastructure backup (in hours)
(time() - rclone_infrastructure_backup_last_success_timestamp) / 3600
# R2 storage usage percentage
rclone_r2_usage_percent
# Total backup errors in last 24h
rclone_infrastructure_backup_errors_24h + rclone_cloud_backup_errors_24h
# Cloud backup sizes by provider
rclone_cloud_backup_size_bytes
# Check if metrics are fresh (collected within last 10 minutes)
time() - rclone_metrics_collection_timestamp < 600
Next Steps¶
- Deploy monitoring stack
- Import dashboards
- Configure alerts (including Rclone backup monitoring)
- Set up on-call rotation
- Document runbooks
- Train team on tools