Monitoring beginner ⏱ 30 minutes

Gatus: Modern Health Dashboard for Your Services

Developer-friendly health monitoring with Kubernetes support. Clean UI and powerful alerting.

By homelabbing.io • 2025-12-16
#gatus#monitoring#health-checks#kubernetes#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:

ToolFocusConfigurationBest For
Uptime KumaWeb UI, ease of usePoint-and-clickNon-technical users, quick setup
GatusYAML config, GitOpsCode-firstDevelopers, infrastructure as code
Prometheus + BlackboxDeep metricsPromQL + YAMLFull observability stack
Healthchecks.ioCron monitoringWeb UI + APIDead 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:

  1. Create Discord webhook (Server Settings → Integrations → Webhooks)
  2. 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:

  1. Create bot with @BotFather
  2. 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

![Website Status](https://gatus.example.com/api/v1/badges/uptime/1h/website.svg)
![API Status](https://gatus.example.com/api/v1/badges/uptime/24h/api.svg)

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: true for 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

FeatureGatusUptime Kuma
ConfigurationYAML filesWeb UI
Setup ComplexityLow (one YAML file)Lower (point-click)
Version ControlYes (GitOps)No (DB-backed)
Protocol SupportHTTP, TCP, DNS, ICMP, SSH, WSHTTP, TCP, Ping, DNS, Docker, Push
Alerting Channels20+ providers95+ providers
Status PageYes, customizableYes, feature-rich
Database RequiredNo (optional)Yes (SQLite/Postgres)
Resource Usage15-30MB RAM100-200MB RAM
Kubernetes IntegrationNative service discoveryManual configuration
Docker MonitoringVia HTTP endpointsNative Docker API
Public APIYes, badges + JSONYes, extensive
2FA SupportNo (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:

  1. Deploy Gatus with basic HTTP checks
  2. Configure Discord or Telegram alerts
  3. 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!