Gatus: Modern Health Dashboard for Your Services
Developer-friendly health monitoring with Kubernetes support. Clean UI and powerful alerting.
Introduction
Service monitoring doesn’t have to be complicated. While tools like Uptime Kuma provide feature-rich UIs and Prometheus offers deep observability, sometimes you need something simpler: a lightweight health checker that just works, configured entirely through YAML, with a beautiful status page you can share.
Enter Gatus—a developer-focused health dashboard that monitors HTTP endpoints, TCP ports, DNS, ICMP pings, and more. It’s gained significant traction in the homelab community (9,000+ GitHub stars) for its elegant simplicity and “configuration as code” philosophy.
What makes Gatus special:
- Configuration as code via YAML files
- Multi-protocol support (HTTP, TCP, DNS, ICMP, gRPC, WebSocket)
- Beautiful public status pages with customization
- 20+ notification providers (Discord, Slack, Email, Telegram, PagerDuty)
- Low resource usage (15-30MB RAM)
- No database required (optional persistence)
- GitOps-friendly deployment
What you’ll learn:
- Deploy Gatus with Docker Compose in under 15 minutes
- Create comprehensive health checks for HTTP, TCP, DNS, and more
- Configure multi-channel alerting (Discord, Slack, Email, PagerDuty)
- Build beautiful public status pages for your services
- Implement incident management and maintenance windows
- Compare Gatus vs Uptime Kuma for different use cases
Prerequisites:
- Docker and Docker Compose installed
- Basic YAML knowledge
- Services you want to monitor
- Optional: Notification service accounts (Discord, Telegram)
Why Gatus?
The Health Monitoring Landscape
Different tools solve different problems:
| Tool | Focus | Configuration | Best For |
|---|---|---|---|
| Uptime Kuma | Web UI, ease of use | Point-and-click | Non-technical users, quick setup |
| Gatus | YAML config, GitOps | Code-first | Developers, infrastructure as code |
| Prometheus + Blackbox | Deep metrics | PromQL + YAML | Full observability stack |
| Healthchecks.io | Cron monitoring | Web UI + API | Dead man’s switch for jobs |
Gatus excels at:
- Configuration version control (GitOps-friendly)
- Reproducible deployments across environments
- Beautiful, customizable status pages
- Low resource usage (15-30MB RAM)
- No database required (optional persistence)
- Perfect for infrastructure-as-code workflows
Key Features
1. Multi-Protocol Support
- HTTP/HTTPS with custom headers, bodies, and status codes
- TCP port connectivity
- DNS resolution validation
- ICMP ping
- SSH connectivity
- WebSocket connections
- gRPC health checks
2. Advanced Conditions
- Response time thresholds
- Status code validation
- Body content matching (regex, JSON path)
- Certificate expiration checks
- Custom scripts/commands
3. Alerting
- 20+ notification providers
- Customizable alert templates
- Failure threshold configuration
- Alert grouping and deduplication
- Webhook integration
4. Status Pages
- Public or private dashboards
- Customizable branding
- Badges for embedding
- Historical uptime charts
- Maintenance mode
Quick Start Deployment
Single-Service Monitoring
Let’s start with the simplest Gatus deployment.
Create project directory:
mkdir -p ~/gatus
cd ~/gatus Create config.yaml:
# Gatus configuration
web:
port: 8080
# Storage (optional, for historical data)
storage:
type: memory # Use 'postgres' or 'sqlite' for persistence
# Define health checks (endpoints)
endpoints:
- name: Google
group: external
url: "https://www.google.com"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 500"
- name: GitHub
group: external
url: "https://github.com"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[CERTIFICATE_EXPIRATION] > 720h" # 30 days
- name: Local API
group: homelab
url: "http://192.168.1.100:8080/health"
interval: 1m
conditions:
- "[STATUS] == 200"
- "[BODY].status == pass"
- "[RESPONSE_TIME] < 100" Create docker-compose.yml:
version: '3.8'
services:
gatus:
image: twinproduction/gatus:latest
container_name: gatus
ports:
- "8080:8080"
volumes:
- ./config.yaml:/config/config.yaml
- gatus-data:/data
restart: unless-stopped
environment:
- TZ=America/New_York
volumes:
gatus-data: Start Gatus:
docker compose up -d Access dashboard:
http://localhost:8080 You’ll see a clean, minimal dashboard showing the health status of your configured endpoints.
Comprehensive Health Checks
HTTP/HTTPS Monitoring
Basic HTTP check:
endpoints:
- name: Website Homepage
url: "https://example.com"
interval: 1m
conditions:
- "[STATUS] == 200" Advanced HTTP with custom headers:
endpoints:
- name: Authenticated API
url: "https://api.example.com/v1/users"
method: GET
headers:
Authorization: Bearer YOUR-TOKEN
Content-Type: application/json
interval: 2m
conditions:
- "[STATUS] == 200"
- "[BODY].success == true"
- "[RESPONSE_TIME] < 1000" POST request with body:
endpoints:
- name: API Health Endpoint
url: "https://api.example.com/health"
method: POST
body: |
{
"check": "full",
"include": ["database", "cache"]
}
headers:
Content-Type: application/json
conditions:
- "[STATUS] == 200"
- "[BODY].database.status == healthy"
- "[BODY].cache.status == healthy" Response body validation:
endpoints:
- name: API Status
url: "https://api.example.com/status"
conditions:
- "[STATUS] == 200"
- "[BODY].version == 2.1.0"
- "[BODY].uptime > 0"
- "len([BODY].errors) == 0"
- "[BODY].active_users > 10" Certificate expiration monitoring:
endpoints:
- name: SSL Certificate Check
url: "https://example.com"
interval: 24h
conditions:
- "[CERTIFICATE_EXPIRATION] > 720h" # Alert if under 30 days
alerts:
- type: discord
description: "SSL certificate expiring soon!"
failure-threshold: 1 TCP Port Monitoring
Database connectivity:
endpoints:
- name: PostgreSQL
url: "tcp://192.168.1.50:5432"
interval: 1m
conditions:
- "[CONNECTED] == true"
- "[RESPONSE_TIME] < 100"
- name: MySQL
url: "tcp://192.168.1.51:3306"
interval: 1m
conditions:
- "[CONNECTED] == true" SSH accessibility:
endpoints:
- name: SSH Server
url: "tcp://192.168.1.100:22"
interval: 5m
conditions:
- "[CONNECTED] == true" DNS Resolution Checks
Verify DNS responses:
endpoints:
- name: DNS Pi-hole
url: "dns://192.168.1.5:53"
dns:
query-name: example.com
query-type: A
interval: 5m
conditions:
- "[DNS_RCODE] == NOERROR"
- "len([BODY]) > 0"
- name: DNS Cloudflare
url: "dns://1.1.1.1:53"
dns:
query-name: example.com
query-type: A
conditions:
- "[DNS_RCODE] == NOERROR"
- "[BODY] == 93.184.216.34" ICMP Ping Monitoring
Network device availability:
endpoints:
- name: Router
url: "icmp://192.168.1.1"
interval: 1m
conditions:
- "[CONNECTED] == true"
- "[RESPONSE_TIME] < 10"
- name: NAS
url: "icmp://192.168.1.100"
interval: 1m
conditions:
- "[CONNECTED] == true" WebSocket Monitoring
Real-time service health:
endpoints:
- name: WebSocket API
url: "ws://localhost:8080/ws"
interval: 30s
conditions:
- "[CONNECTED] == true" Notification Configuration
Discord Notifications
Setup:
- Create Discord webhook (Server Settings → Integrations → Webhooks)
- Copy webhook URL
Configuration:
# Global alerting configuration
alerting:
discord:
webhook-url: "https://discord.com/api/webhooks/YOUR-WEBHOOK-URL"
default-alert:
description: "Service health alert"
failure-threshold: 3
success-threshold: 2
endpoints:
- name: Critical API
url: "https://api.example.com"
interval: 1m
conditions:
- "[STATUS] == 200"
alerts:
- type: discord
description: "API is down!"
failure-threshold: 2
success-threshold: 1 Custom alert message:
alerting:
discord:
webhook-url: "YOUR-WEBHOOK"
default-alert:
description: |
[ENDPOINT_NAME] is [ALERT_TRIGGERED_OR_RESOLVED]
Response time: [RESPONSE_TIME]ms
Status: [STATUS]
failure-threshold: 3 Slack Integration
alerting:
slack:
webhook-url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
default-alert:
description: "[ENDPOINT_NAME] health check failed"
failure-threshold: 3
success-threshold: 2
endpoints:
- name: Production API
url: "https://api.example.com"
alerts:
- type: slack
description: "Production API is DOWN" Telegram Alerts
Setup:
- Create bot with @BotFather
- Get bot token and chat ID
Configuration:
alerting:
telegram:
token: "YOUR-BOT-TOKEN"
id: "YOUR-CHAT-ID"
default-alert:
description: "[ENDPOINT_NAME]: [ALERT_DESCRIPTION]"
failure-threshold: 2
endpoints:
- name: Homelab Server
url: "https://homelab.example.com"
alerts:
- type: telegram Email Notifications
alerting:
email:
from: "[email protected]"
username: "[email protected]"
password: "YOUR-PASSWORD"
host: "smtp.gmail.com"
port: 587
to: "[email protected]"
default-alert:
description: "Service health alert"
failure-threshold: 3 PagerDuty Integration
alerting:
pagerduty:
integration-key: "YOUR-INTEGRATION-KEY"
default-alert:
description: "[ENDPOINT_NAME] is down"
failure-threshold: 1 # Immediate alert Multi-Channel Alerting
Route different severity to different channels:
alerting:
discord:
webhook-url: "DISCORD-WEBHOOK"
telegram:
token: "TELEGRAM-TOKEN"
id: "TELEGRAM-CHAT-ID"
pagerduty:
integration-key: "PAGERDUTY-KEY"
endpoints:
# Critical: Alert on all channels
- name: Production Database
url: "tcp://db.prod:5432"
alerts:
- type: discord
failure-threshold: 1
- type: telegram
failure-threshold: 1
- type: pagerduty
failure-threshold: 1
# Warning: Discord only
- name: Development API
url: "https://api.dev.example.com"
alerts:
- type: discord
failure-threshold: 5 Creating Status Pages
Basic Public Status Page
Enable status page:
web:
port: 8080
address: 0.0.0.0
ui:
title: "Homelab Status"
header: "Infrastructure Health Dashboard"
logo: "https://example.com/logo.png"
link: "https://example.com"
endpoints:
- name: Website
group: public
url: "https://example.com"
interval: 1m
conditions:
- "[STATUS] == 200"
- name: API
group: public
url: "https://api.example.com"
interval: 1m
conditions:
- "[STATUS] == 200" Custom CSS Styling
ui:
title: "Homelab Status"
custom-css: |
.container {
max-width: 1200px;
}
.endpoint-group h2 {
color: #667eea;
}
.endpoint {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} Badge Generation
Embed badges in README:
# My Homelab

 Badge types:
- Uptime:
/api/v1/badges/uptime/DURATION/ENDPOINT.svg - Response time:
/api/v1/badges/response-time/DURATION/ENDPOINT.svg - Health:
/api/v1/badges/health/ENDPOINT.svg
Public vs Private Status Pages
Public status page configuration:
ui:
title: "Public Services Status"
# No authentication required
endpoints:
- name: Website
group: public-services
url: "https://example.com"
ui:
hide-hostname: true # Hide internal details
hide-url: true Private status page with authentication: Use a reverse proxy with basic auth:
location / {
auth_basic "Restricted Status Page";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:8080;
} Service Groups
Organize services into logical groups:
endpoints:
# External Services Group
- name: Website
group: external
url: "https://example.com"
conditions:
- "[STATUS] == 200"
- name: CDN
group: external
url: "https://cdn.example.com"
conditions:
- "[STATUS] == 200"
# Internal Services Group
- name: PostgreSQL
group: internal-databases
url: "tcp://postgres:5432"
conditions:
- "[CONNECTED] == true"
- name: Redis
group: internal-databases
url: "tcp://redis:6379"
conditions:
- "[CONNECTED] == true"
# Monitoring Stack Group
- name: Grafana
group: monitoring
url: "http://grafana:3000/api/health"
conditions:
- "[STATUS] == 200" Incident Management
Maintenance Mode
Schedule maintenance windows:
endpoints:
- name: API Server
url: "https://api.example.com"
conditions:
- "[STATUS] == 200"
# Disable monitoring during maintenance
disable-monitoring-lock: false
# Or use time-based conditions (2 AM - 4 AM)
conditions:
- "[STATUS] == 200 || [TIME] >= 02:00 && [TIME] <= 04:00" Incident Response
Webhook for incident management:
alerting:
custom:
url: "https://your-incident-system.com/webhook"
method: POST
body: |
{
"service": "[ENDPOINT_NAME]",
"status": "[ALERT_TRIGGERED_OR_RESOLVED]",
"timestamp": "[ALERT_TIMESTAMP]",
"response_time": "[RESPONSE_TIME]"
}
headers:
Authorization: Bearer YOUR-TOKEN
Content-Type: application/json Alert Escalation
Progressive alert escalation:
endpoints:
- name: Critical Production Service
url: "https://production.example.com"
alerts:
# Level 1: Discord notification after 2 failures
- type: discord
failure-threshold: 2
description: "⚠️ Service degraded"
# Level 2: Telegram after 3 failures
- type: telegram
failure-threshold: 3
description: "🚨 Service down"
# Level 3: PagerDuty after 5 failures
- type: pagerduty
failure-threshold: 5
description: "🔴 Critical outage" Advanced Configuration
Persistent Storage
PostgreSQL backend:
storage:
type: postgres
path: "postgres://user:password@postgres:5432/gatus?sslmode=disable"
caching: true SQLite for local persistence:
storage:
type: sqlite
path: /data/gatus.db docker-compose.yml with PostgreSQL:
version: '3.8'
services:
gatus:
image: twinproduction/gatus:latest
volumes:
- ./config.yaml:/config/config.yaml
ports:
- "8080:8080"
depends_on:
- postgres
environment:
- TZ=America/New_York
postgres:
image: postgres:15-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=gatus
- POSTGRES_USER=gatus
- POSTGRES_PASSWORD=secure_password
restart: unless-stopped
volumes:
postgres-data: Environment Variables
Use environment variables for secrets:
Create .env file:
DISCORD_WEBHOOK=https://discord.com/api/webhooks/YOUR-WEBHOOK
TELEGRAM_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=-987654321
DB_PASSWORD=secure_password Reference in config.yaml:
alerting:
discord:
webhook-url: "${DISCORD_WEBHOOK}"
telegram:
token: "${TELEGRAM_TOKEN}"
id: "${TELEGRAM_CHAT_ID}"
storage:
type: postgres
path: "postgres://gatus:${DB_PASSWORD}@postgres:5432/gatus" Update docker-compose.yml:
services:
gatus:
image: twinproduction/gatus:latest
env_file:
- .env
volumes:
- ./config.yaml:/config/config.yaml Client Configuration
Customize HTTP behavior:
endpoints:
- name: Slow API
url: "https://slow-api.example.com"
client:
timeout: 30s
insecure: false # Allow self-signed certs
ignore-redirect: false
max-redirects: 3
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 30000" Custom Scripts
Run external health checks:
endpoints:
- name: Backup Status
url: "http://localhost" # Dummy URL
interval: 1h
conditions:
- "[STATUS] == 200"
client:
command: |
#!/bin/bash
if [ -f /backups/last-backup.txt ]; then
age=$(($(date +%s) - $(stat -c %Y /backups/last-backup.txt)))
if [ $age -lt 86400 ]; then
exit 0 # Success
fi
fi
exit 1 # Failure Production Configuration Example
Complete production-ready setup:
# config.yaml - Production configuration
web:
port: 8080
address: 0.0.0.0
storage:
type: postgres
path: "postgres://gatus:${DB_PASSWORD}@postgres:5432/gatus?sslmode=require"
caching: true
ui:
title: "Homelab Infrastructure Status"
header: "Real-time Health Monitoring"
logo: "https://example.com/logo.png"
link: "https://status.example.com"
metrics: true # Enable Prometheus metrics at /metrics
alerting:
discord:
webhook-url: "${DISCORD_WEBHOOK}"
default-alert:
description: "[ENDPOINT_NAME] - [ALERT_TRIGGERED_OR_RESOLVED]"
failure-threshold: 3
success-threshold: 2
send-on-resolved: true
telegram:
token: "${TELEGRAM_TOKEN}"
id: "${TELEGRAM_CHAT_ID}"
default-alert:
failure-threshold: 1 # Critical alerts
email:
from: "[email protected]"
username: "${EMAIL_USERNAME}"
password: "${EMAIL_PASSWORD}"
host: "smtp.gmail.com"
port: 587
to: "[email protected]"
default-alert:
failure-threshold: 5
endpoints:
# External Dependencies
- name: Cloudflare DNS
group: external
url: "dns://1.1.1.1:53"
dns:
query-name: example.com
query-type: A
interval: 5m
conditions:
- "[DNS_RCODE] == NOERROR"
# Public Services
- name: Website
group: public
url: "https://example.com"
interval: 1m
conditions:
- "[STATUS] == 200"
- "[RESPONSE_TIME] < 2000"
- "[CERTIFICATE_EXPIRATION] > 720h"
alerts:
- type: discord
- type: telegram
failure-threshold: 1
- name: API Gateway
group: public
url: "https://api.example.com/health"
interval: 30s
conditions:
- "[STATUS] == 200"
- "[BODY].status == healthy"
- "[RESPONSE_TIME] < 500"
alerts:
- type: telegram
- type: email
# Internal Services
- name: PostgreSQL
group: databases
url: "tcp://postgres:5432"
interval: 1m
conditions:
- "[CONNECTED] == true"
- "[RESPONSE_TIME] < 100"
alerts:
- type: discord
- type: telegram
failure-threshold: 1
- name: Redis Cache
group: databases
url: "tcp://redis:6379"
interval: 1m
conditions:
- "[CONNECTED] == true"
# Monitoring Stack
- name: Grafana
group: monitoring
url: "http://grafana:3000/api/health"
interval: 1m
conditions:
- "[STATUS] == 200"
- "[BODY].database == ok"
- name: Prometheus
group: monitoring
url: "http://prometheus:9090/-/healthy"
interval: 2m
conditions:
- "[STATUS] == 200"
# Network Infrastructure
- name: Router
group: network
url: "icmp://192.168.1.1"
interval: 30s
conditions:
- "[CONNECTED] == true"
- "[RESPONSE_TIME] < 10"
- name: Pi-hole DNS
group: network
url: "http://192.168.1.5/admin/api.php"
interval: 1m
conditions:
- "[STATUS] == 200"
- "[BODY].status == enabled" Troubleshooting
Issue 1: Endpoints Always Show as Down
Diagnosis:
# Check Gatus logs
docker logs gatus
# Test endpoint manually
curl -I https://example.com
# Verify network connectivity from container
docker exec gatus ping example.com Common causes:
- Firewall blocking outbound connections
- DNS resolution issues
- SSL certificate problems (use
insecure: truefor testing)
Issue 2: Alerts Not Sending
Diagnosis:
# Check webhook URL is correct
curl -X POST YOUR-DISCORD-WEBHOOK -H "Content-Type: application/json" -d '{"content":"Test message"}'
# Verify environment variables loaded
docker exec gatus env | grep DISCORD Issue 3: High Memory Usage
Solution:
# Limit retention in memory storage
storage:
type: memory
# Or use database for efficiency
type: postgres Issue 4: Configuration Errors
Validate YAML syntax:
# Check for syntax errors
docker logs gatus | grep -i error
# Validate YAML locally
yamllint config.yaml Common mistakes:
- Missing quotes around webhook URLs
- Incorrect indentation
- Invalid JSON in body fields
- Wrong alert types
Gatus vs Uptime Kuma
Feature Comparison
| Feature | Gatus | Uptime Kuma |
|---|---|---|
| Configuration | YAML files | Web UI |
| Setup Complexity | Low (one YAML file) | Lower (point-click) |
| Version Control | Yes (GitOps) | No (DB-backed) |
| Protocol Support | HTTP, TCP, DNS, ICMP, SSH, WS | HTTP, TCP, Ping, DNS, Docker, Push |
| Alerting Channels | 20+ providers | 95+ providers |
| Status Page | Yes, customizable | Yes, feature-rich |
| Database Required | No (optional) | Yes (SQLite/Postgres) |
| Resource Usage | 15-30MB RAM | 100-200MB RAM |
| Kubernetes Integration | Native service discovery | Manual configuration |
| Docker Monitoring | Via HTTP endpoints | Native Docker API |
| Public API | Yes, badges + JSON | Yes, extensive |
| 2FA Support | No (use reverse proxy) | Yes, built-in |
Use Case Recommendations
Choose Gatus if:
- You prefer infrastructure as code (GitOps)
- You’re managing multiple environments (dev/staging/prod)
- You need minimal resource usage
- You’re comfortable with YAML configuration
- You value reproducible deployments
- You want to version control your monitoring setup
Choose Uptime Kuma if:
- You prefer graphical configuration
- You need quick setup without coding
- You want built-in Docker container monitoring
- You need extensive notification providers (95+)
- You’re managing services for non-technical users
- You want push-based monitoring (Dead Man’s Switch)
Use both together:
- Gatus for infrastructure/K8s services (GitOps)
- Uptime Kuma for ad-hoc monitoring and user-facing status
Conclusion
Gatus brings developer-friendly, GitOps-compatible health monitoring to homelabs. Its YAML-first approach makes it perfect for infrastructure-as-code workflows, while the beautiful status pages and extensive alerting keep your services visible and reliable.
Key takeaways:
- Configuration as code enables version control and reproducibility
- Multi-protocol support covers HTTP, TCP, DNS, ICMP, and more
- 20+ alerting providers ensure you’re notified on your preferred channels
- Beautiful public status pages with customization
- 15-30MB RAM footprint makes it homelab-friendly
Your deployment timeline:
Week 1:
- Deploy Gatus with basic HTTP checks
- Configure Discord or Telegram alerts
- Create public status page
Week 2: 4. Add comprehensive health checks for all services 5. Implement multi-channel alerting strategy 6. Set up PostgreSQL for historical data
Week 3: 7. Customize status page branding 8. Create badges for documentation 9. Implement maintenance windows
Resources:
Whether you’re monitoring a single server or a complex Kubernetes cluster, Gatus provides elegant, code-first health monitoring that scales with your infrastructure.
Happy monitoring!