How to Self-Host Gatus with Docker Compose and Discord Alerts: Automated Service Health Dashboard

DevOps-Monitoring-Spezialistin überwacht Server-Uptime und Status-Dashboards mit Gatus und Discord Alerts in Docker Compose
How to Self-Host Gatus with Docker Compose and Discord Alerts: Automated Service Health Dashboard 3

In distributed homelabs, microservice clusters, and production environments, knowing that a server or container is running is not enough. A container can report a healthy Docker status while its internal HTTP endpoint returns 502 Bad Gateway, its SSL certificate has silently expired, or its database queries exceed acceptable latency thresholds. Traditional monitoring suites like Prometheus, Grafana, and Datadog provide deep time-series metrics, but configuring simple uptime checks, status pages, and instant incident notifications with them introduces massive architectural overhead.

Gatus is an ultra-lightweight, automated service health monitoring engine and status dashboard written in Go. Unlike web-configured tools like Uptime Kuma, Gatus embraces a pure Declarative GitOps configuration model: your entire monitoring infrastructure—endpoints, response-time assertions, TLS verification, body evaluations, and alert rules—is defined in a single, version-controlled YAML file. Consuming less than 30 MB of RAM, Gatus can test hundreds of internal and external endpoints and deliver rich, actionable incident alerts directly to Discord, Slack, Telegram, or PagerDuty.

Architecture & Assertion Pipeline

Gatus operates as an autonomous testing engine. At configured intervals, worker routines execute health evaluations across diverse protocols, compare raw responses against declarative condition expressions, record historical latency and uptime into an embedded SQLite database, and trigger alerts upon threshold breaches:

+-------------------------------------------------------------------------------+
|                            GATUS MONITORING ENGINE                            |
|             (Ultra-lightweight Go process, RAM < 30MB, Port 8080)             |
+-------------------------------------------------------------------------------+
       |                   |                   |                   |
       | HTTP/HTTPS        | ICMP Ping         | TCP Sockets       | DNS Resolution
       v                   v                   v                   v
+--------------+    +--------------+    +--------------+    +--------------+
| Web APIs &   |    | Core Routers |    | Database     |    | DNS Servers  |
| Microservices|    | & Gateways   |    | Endpoints    |    | (AdGuard,    |
| (JSON / TLS) |    | (Packet Loss)|    | (Postgres)   |    | Pi-hole)     |
+--------------+    +--------------+    +--------------+    +--------------+
       \                   |                   /                   /
        \                  |                  /                   /
         v                 v                 v                   v
+-------------------------------------------------------------------------------+
|                        DECLARATIVE ASSERTION EVALUATION                       |
|   - [STATUS] == 200                                                           |
|   - [RESPONSE_TIME] < 300ms                                                   |
|   - [CERTIFICATE_EXPIRATION] > 48h                                            |
|   - [BODY].status == "healthy"                                                |
+-------------------------------------------------------------------------------+
         |                                                     |
         v                                                     v
+-----------------------------+               +---------------------------------+
|   Embedded SQLite Storage   |               |     Discord Alert Engine        |
|  Status Dashboard Web UI    |               |  (Rich embeds on failure/heal)  |
+-----------------------------+               +---------------------------------+

Step 1: Directory Setup & File Permissions

Create a structured deployment directory on your Docker host. Gatus uses a volume for persistent storage (SQLite database) and a separate volume for its read-only configuration:

sudo mkdir -p /opt/gatus/config
sudo mkdir -p /opt/gatus/data

cd /opt/gatus

By default, the official Gatus container runs as non-root user nobody (UID 65534) for defense-in-depth security. Set the ownership of the persistent data directory accordingly:

sudo chown -R 65534:65534 /opt/gatus/data
sudo chmod 700 /opt/gatus/data

Step 2: Crafting the Declarative config.yaml

Create the master configuration file at /opt/gatus/config/config.yaml. This configuration defines the web UI, embedded SQLite persistence, Discord alerting integration, and multi-protocol endpoint health checks:

storage:
  type: sqlite
  path: /data/data.db

web:
  port: 8080

ui:
  title: "Infrastructure Status | Homelab & Cloud"
  description: "Automated real-time service health monitoring"
  header: "Operations Health Dashboard"

alerting:
  discord:
    webhook-url: "${DISCORD_WEBHOOK_URL}"
    default-alert:
      failure-threshold: 3
      success-threshold: 2
      send-on-resolved: true

endpoints:
  - name: "Public Web Portal"
    group: "Core Web"
    url: "https://example.com"
    interval: 30s
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 400"
      - "[CERTIFICATE_EXPIRATION] > 72h"
    alerts:
      - type: discord

  - name: "Authentication API Health"
    group: "Core Services"
    url: "https://api.example.com/health"
    interval: 15s
    conditions:
      - "[STATUS] == 200"
      - "[BODY].status == ok"
      - "[RESPONSE_TIME] < 250"
    alerts:
      - type: discord

  - name: "Internal DNS Resolver"
    group: "Network Infrastructure"
    url: "192.168.1.1"
    interval: 30s
    dns:
      query-name: "gateway.internal"
      query-type: "A"
    conditions:
      - "[DNS_RCODE] == NOERROR"
    alerts:
      - type: discord

  - name: "PostgreSQL Database Socket"
    group: "Data Storage"
    url: "tcp://192.168.1.50:5432"
    interval: 20s
    conditions:
      - "[CONNECTED] == true"
      - "[RESPONSE_TIME] < 50"
    alerts:
      - type: discord

Notice the power of Gatus’s assertion engine:

  • [STATUS] == 200: Confirms standard HTTP OK status.
  • [RESPONSE_TIME] < 400: Enforces that requests complete within 400 milliseconds.
  • [CERTIFICATE_EXPIRATION] > 72h: Automatically fails if the TLS certificate has less than 3 days remaining before expiration.
  • [BODY].status == ok: Parses JSON payloads and inspects specific attributes without writing external scrapers.
  • failure-threshold: 3: Prevents alert flapping by requiring 3 consecutive failed checks before firing a Discord notification.

Step 3: Setting Up Environment Variables

Never hardcode webhook URLs inside your configuration files. Store sensitive credentials in /opt/gatus/.env:

# Discord Channel Webhook URL
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/your_secret_webhook_token_here
sudo chmod 600 /opt/gatus/.env

Step 4: Production-Grade Docker Compose Stack

Create /opt/gatus/docker-compose.yml:

services:
  gatus:
    image: twinproduction/gatus:v5.16.0
    container_name: gatus
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    env_file:
      - /opt/gatus/.env
    volumes:
      - /opt/gatus/config/config.yaml:/config/config.yaml:ro
      - /opt/gatus/data:/data
    networks:
      - monitoring-net
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_RAW # Enables ICMP Ping checks inside container
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 5s

networks:
  monitoring-net:
    name: monitoring-net
    driver: bridge

Launch Gatus and verify the initialization logs:

cd /opt/gatus
docker compose up -d

# Check startup output
docker compose logs -f gatus

You should see confirmation that the SQLite database has been opened and the evaluation routines have started polling your endpoints.

Step 5: Hardened Reverse Proxy (Caddy & Nginx)

To serve your Gatus status dashboard securely with automatic SSL and optional HTTP Basic Authentication, route traffic through a reverse proxy.

Caddyfile Configuration

status.example.com {
    encode gzip zstd

    # Optional: Restrict status dashboard to authorized team members
    # basicauth {
    #     admin $2a$14$J8Z...hashed_password...
    # }

    reverse_proxy 127.0.0.1:8080 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
    }

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
    }
}

Nginx Configuration

server {
    listen 443 ssl http2;
    server_name status.example.com;

    ssl_certificate /etc/letsencrypt/live/status.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/status.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Step 6: Discord Alert Verification & Badge Integration

To test that Discord alerts fire correctly, temporarily point an endpoint condition to an invalid assertion (such as [STATUS] == 500). After 3 consecutive checks (the configured failure-threshold), Gatus generates an interactive Discord embed containing:

  • Target endpoint name and group
  • Failing assertion rule (e.g., Condition "[STATUS] == 200" failed: got 502)
  • Exact response timestamp and measured round-trip time
  • Direct URL link back to the affected service

When the service recovers and passes 2 consecutive checks (success-threshold: 2), Gatus immediately posts a green recovery notification: ALERT: Public Web Portal has recovered.

Gatus also natively exposes Shields.io-compatible SVG status badges. You can embed real-time health indicators directly into your GitHub READMEs or internal wiki pages:

![API Health](https://status.example.com/api/v1/endpoints/Core%20Services_Authentication%20API%20Health/badge.svg)

Troubleshooting Common Issues

1. Discord Alerts Refused: “HTTP 429 Too Many Requests”

Cause: If multiple endpoints fail simultaneously and have short intervals (e.g. 5s) without failure thresholds, Discord’s API rate limits the incoming webhook events.

Solution: Always configure a sensible failure-threshold: 3 and set check intervals to at least 15s or 30s. This buffers transient network blips and respects Discord webhook quota limits.

2. ICMP Ping Fails: “Operation not permitted” Inside Container

Cause: Docker unprivileged containers drop raw socket capabilities (NET_RAW), preventing the Go binary from sending ICMP echo request packets.

Solution: Add cap_add: - NET_RAW under the gatus service definition in your docker-compose.yml file. This safely delegates ICMP socket permissions without granting full root container privileges.

3. SQLite “Database is Locked” Error

Cause: Storing the SQLite database directory over network file systems (such as NFS or SMB shares) breaks POSIX advisory byte-range locking mechanisms.

Solution: Ensure /opt/gatus/data resides on local block storage (ext4, XFS, or ZFS). Alternatively, if multi-host redundancy is required, configure Gatus to use an external PostgreSQL database in config.yaml.

Conclusion

Gatus strikes the perfect balance between minimal resource consumption and production-grade monitoring capability. By defining checks and alert pipelines declaratively in YAML, you eliminate configuration drift, gain complete visibility into endpoint health and SSL certificate validity, and equip your team with instant, actionable Discord notifications.