Gatus: 50 endpoints, 40MB RAM, free status page for self-hosters

Gatus is a single-binary monitoring tool that probes your services and shows a public status page at a URL you control. You define every check in one YAML file. So your whole setup can live in Git next to the rest of your stack. There is no need for a database, no web UI to click through, and no per-monitor pricing. If you self-host a blog, a Gitea instance , a Home Assistant server, or a mail relay, Gatus gives you a simple way to know when something breaks.

This guide walks through installing Gatus with Docker Compose, writing endpoint checks, setting up alerts for downtime, and tuning the status page for public access.

Why Gatus Over Uptime Kuma, Statping, or Paid Services

The self-hosted monitoring space has a handful of well-known options. The right pick depends on how you manage your infrastructure.

Uptime Kuma is the most popular choice, with over 84,000 GitHub stars. It has a polished web UI where you add monitors through a browser. It works well for people who prefer clicking over editing config files. The tradeoff is that all config lives in a SQLite database. You cannot version-control your monitor definitions, and you cannot review changes in a pull request. Restoring a setup from backup means restoring the database file. If you treat your infrastructure as code, with Terraform, Ansible, and Docker Compose files checked into Git, Uptime Kuma’s approach feels like a gap in the pipeline.

Statping-ng was once a fair pick, but its last real commit landed in 2023. The project is now dead. Running stale monitoring software creates the exact kind of failure you are trying to catch.

Paid services like Pingdom, Better Uptime, and Checkly cost between $20 and $100 per month once you pass a handful of monitors. They offer geographic distribution, synthetic browser testing, and incident workflows that Gatus does not attempt. If you need Playwright-based synthetic checks from five continents with on-call rotation, a paid service or Grafana OnCall is the right tool. But for monitoring 20-80 endpoints from a single location on your own network, Gatus does the job without a subscription.

Gatus sits in a specific niche: config-as-code, low resource use, flexible condition checks, and zero cost. It supports HTTP, TCP, DNS, ICMP, and SSH checks. It tests conditions against status codes, response times, response body content (including JSONPath), TLS certificate expiry, and DNS resolution results. It groups endpoints, exposes an API for uptime badges, and ships a built-in status page that auto-refreshes. Running on a Raspberry Pi 4 monitoring 50 endpoints every 60 seconds, it uses about 40 MB of RAM and almost no CPU.

FeatureGatusUptime KumaPaid (Checkly/Pingdom)
ConfigurationYAML file (Git-friendly)Web UI / SQLite DBWeb UI / API
Self-hostedYesYesNo
Protocol supportHTTP, TCP, DNS, ICMP, SSHHTTP(s), TCP, ICMP, DNSHTTP, browser synthetic
Alerting providers20+ (Slack, Discord, Ntfy, PagerDuty, etc.)90+Varies by vendor
Status pageBuilt-inBuilt-inBuilt-in
Resource usage~40 MB RAM for 50 endpoints~100-150 MB RAMN/A (cloud)
CostFreeFree$20-100+/month
Geo-distributed checksNoNoYes
Config version controlNative (YAML in Git)Manual DB exportsAPI-based

When is Gatus not the right choice? If you need browser-based checks, checks from many places at once, or built-in incident handling with on-call rotas, look elsewhere.

Installing and Configuring Gatus with Docker Compose

Gatus runs as a single container with one mounted config file. The current stable release is v5.34.0, shipped as twinproduction/gatus:latest on Docker Hub.

Create a project directory and add two files: docker-compose.yml and a config file at config/config.yaml.

# docker-compose.yml
services:
  gatus:
    image: twinproduction/gatus:v5.34.0
    container_name: gatus
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./config:/config:ro
      - gatus-data:/data
    environment:
      - TZ=America/New_York
      - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL}
      - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL}
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  gatus-data:

The config/config.yaml file is where all monitoring logic lives. Here is a small starting point:

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

ui:
  title: "Service Status"
  header: "Service Status"
  description: "Real-time availability of all services"

endpoints:
  - name: Blog
    group: Public
    url: "https://example.com"
    interval: 60s
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 1000"
      - "[CERTIFICATE_EXPIRATION] > 168h"

The storage block turns on persistent history. Without it, Gatus keeps check results in memory, and a container restart wipes everything. SQLite is the simplest option, and Gatus creates the database file for you. For larger setups, or when you want to query monitoring data from other tools, Gatus also supports PostgreSQL:

storage:
  type: postgres
  path: "postgres://gatus:password@postgres:5432/gatus?sslmode=disable"

Env variable substitution works throughout the config file. Any ${ENV_VAR} reference is swapped at startup for the matching env variable. This keeps secrets like webhook URLs out of your Git repo. Pass them through the environment block in Docker Compose or a .env file.

The healthcheck in the Compose file hits Gatus’s built-in /health endpoint. This lets Docker, or your orchestrator, know when Gatus itself is unhealthy. For production setups, monitor Gatus from an external service or a second Gatus instance on a different host. This solves the “who watches the watchmen” problem.

Start the stack with docker compose up -d and open http://your-host:8080 to see the status page.

Gatus dashboard showing endpoint groups with uptime history and response time graphs
The Gatus status page with dark mode, displaying grouped endpoints and historical uptime data

Configuring Endpoint Checks and Conditions

The condition system is where Gatus earns its name. Each endpoint defines a list of conditions in a bracketed syntax. Gatus checks every one on each interval. If any check fails, the endpoint is marked as down.

HTTP Checks

Most monitors will be HTTP checks against web applications and APIs:

endpoints:
  - name: API Health
    group: Backend
    url: "https://api.example.com/health"
    interval: 30s
    conditions:
      - "[STATUS] == 200"
      - "[BODY].status == ok"
      - "[RESPONSE_TIME] < 500"

Gatus parses JSON response bodies on its own when the content type is application/json. The [BODY].status syntax uses JSONPath to reach into the response. You can also use [BODY] == pat(*expected*) to match patterns in non-JSON responses.

TCP Checks

TCP checks confirm that a service accepts connections on a given port. They do not care about the protocol spoken on top:

  - name: PostgreSQL
    group: Infrastructure
    url: "tcp://db.example.com:5432"
    interval: 60s
    conditions:
      - "[CONNECTED] == true"
      - "[RESPONSE_TIME] < 100"

This is handy for databases, message brokers, and any service where you just need to confirm the port is open and fast to respond.

DNS Checks

DNS checks query a resolver and assert on the result. This helps you catch propagation issues after DNS changes:

  - name: DNS Resolution
    group: Infrastructure
    url: "dns://1.1.1.1"
    interval: 300s
    dns:
      query-name: "example.com"
      query-type: "A"
    conditions:
      - "[DNS_RCODE] == NOERROR"
      - "[BODY] == 93.184.216.34"

DNS checks are a good fit for monitoring a local Pi-hole and Unbound resolver . They confirm your ad-blocking DNS still responds correctly after an update or restart.

ICMP and SSH Checks

ICMP checks monitor network reachability for LAN devices:

  - name: Router
    group: Network
    url: "icmp://192.168.1.1"
    interval: 30s
    conditions:
      - "[CONNECTED] == true"
      - "[RESPONSE_TIME] < 10"

  - name: Jump Server SSH
    group: Infrastructure
    url: "ssh://server.example.com:22"
    interval: 120s
    conditions:
      - "[CONNECTED] == true"

Note that ICMP checks need Gatus to run with the CAP_NET_RAW capability or as root. In Docker, add cap_add: [NET_RAW] to the service definition.

Gatus condition evaluation detail showing pass and fail states for individual endpoint checks
Expanded endpoint view with individual condition results for status code, response time, and certificate expiry

Endpoint Groups and Intervals

Group endpoints with the group field. The status page renders groups as collapsible sections. Set shorter intervals (30s) for critical public-facing services and longer intervals (300s) for less critical internal checks. Gatus staggers checks on its own to avoid burst traffic. Keep intervals above 10 seconds, so you do not trip rate limits or get flagged by your own WAF.

Setting Up Alerts for Downtime and Degradation

A status page shows current and past state. But you need push notifications to act on outages before your users notice. Gatus supports over 20 alerting providers. Here are the most useful ones for a self-hosted setup.

Alert Trigger Behavior

Each endpoint can define multiple alert targets with independent thresholds:

endpoints:
  - name: Blog
    group: Public
    url: "https://example.com"
    interval: 60s
    conditions:
      - "[STATUS] == 200"
    alerts:
      - type: slack
        failure-threshold: 3
        success-threshold: 2
      - type: discord
        failure-threshold: 5
        success-threshold: 3

The failure-threshold sets how many failures in a row must happen before an alert fires. Setting it to 3 means Gatus tolerates two brief failures before it notifies you. The success-threshold sets how many successes in a row are needed before a recovery alert goes out. This prevents flapping, the rapid back-and-forth between alert and recovery messages during patchy issues.

Gatus sends exactly one notification per state change. It does not spam you on every failed check. A single “Blog is DOWN” message goes out when the threshold is crossed, and a single “Blog is UP” message follows once the endpoint recovers.

Configuring Notification Providers

Define providers in the alerting section of the config:

alerting:
  slack:
    webhook-url: "${SLACK_WEBHOOK_URL}"
    default-alert:
      failure-threshold: 3
      success-threshold: 2

  discord:
    webhook-url: "${DISCORD_WEBHOOK_URL}"

  ntfy:
    topic: "monitoring"
    url: "https://ntfy.example.com"
    priority: 4

  email:
    from: "gatus@example.com"
    host: "smtp.example.com"
    port: 587
    username: "${SMTP_USER}"
    password: "${SMTP_PASS}"
    to: "admin@example.com"

Ntfy deserves a special mention for self-hosted setups. It is a free, self-hostable push notification service that sends alerts straight to your phone. Gatus has had native Ntfy support since v5.x, so you no longer need the custom provider workaround. Just set the topic, url, and optional priority (a 1-5 scale, where 5 is urgent).

For Slack and Discord, create an incoming webhook in your workspace or server settings and pass the URL through an environment variable. Gatus formats each message with the endpoint name, URL, failed condition, and response time.

Gatus Slack notification showing endpoint failure with service name, URL, and condition details
Slack alert message from Gatus reporting a failed endpoint check

Email alerts work as a fallback channel. If your Slack workspace or Discord server is also having issues, email sent through a different provider can still reach you.

For teams running production workloads, Gatus also offers PagerDuty integration:

alerting:
  pagerduty:
    integration-key: "${PAGERDUTY_KEY}"

Gatus sends PagerDuty events that kick off incident workflows, escalation policies, and on-call schedules.

Customizing the Status Page and Exposing It Publicly

The built-in Gatus status page is clean and useful out of the box. It shows each endpoint grouped by category, with colored dots for current status and an uptime history graph when persistent storage is on.

UI Configuration

Customize the page appearance through the ui section:

ui:
  title: "Homelab Status"
  header: "Service Status"
  description: "Real-time availability of all homelab services"
  logo: "https://example.com/logo.png"

The page auto-refreshes every 60 seconds. With SQLite or PostgreSQL storage, the page shows uptime percentages for the last 7 days and 30 days next to response time graphs. That is the kind of history that builds trust with users.

Status Badges

Gatus exposes badge endpoints that you can embed in GitHub READMEs, doc sites, or blog sidebars:

![Uptime](https://status.example.com/api/v1/endpoints/public_blog/uptimes/7d/badge.svg)
![Response Time](https://status.example.com/api/v1/endpoints/public_blog/response-times/24h/badge.svg)

The badge URL pattern is /api/v1/endpoints/{group}_{name}/uptimes/{duration}/badge.svg. Group and name are lowercased with spaces replaced by hyphens.

Public Exposure with a Reverse Proxy

For public-facing status pages, put Gatus behind a reverse proxy like Traefik or Nginx . With Traefik, add labels to the Compose service:

services:
  gatus:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.gatus.rule=Host(`status.example.com`)"
      - "traefik.http.routers.gatus.tls.certresolver=letsencrypt"
      - "traefik.http.services.gatus.loadbalancer.server.port=8080"

If you want to lock down certain API endpoints while keeping the status page public, set up path-based rules in your reverse proxy. The status page is served at /, while the API lives under /api/v1/. The same proxy pattern works for other self-hosted analytics tools. Our privacy-first analytics guide shows the same Caddy-based setup for a metrics stack.

Monitoring Internal Docker Services

One handy perk of running Gatus in Docker is that it can reach other containers on the same network by their service name. Instead of exposing ports to the host just for monitoring, create a shared Docker network:

networks:
  monitoring:
    external: true

services:
  gatus:
    networks:
      - monitoring

Then reference internal services in your endpoints:

endpoints:
  - name: Gitea
    group: Internal
    url: "http://gitea:3000"
    interval: 60s
    conditions:
      - "[STATUS] == 200"

This keeps your service ports hidden from the host while you still monitor them.

Integrating Gatus with Prometheus and Grafana

Gatus exposes a /metrics endpoint in Prometheus format. So you can scrape it with Prometheus and build dashboards in Grafana for long-term metrics and richer charts.

Add a Prometheus scrape target:

# prometheus.yml
scrape_configs:
  - job_name: gatus
    scrape_interval: 60s
    static_configs:
      - targets: ["gatus:8080"]

The metrics include endpoint health, response times, and certificate expiry data. You can add custom labels to endpoints with the extra-labels field in the Gatus config. This gives you more flexible filters in Grafana queries.

There is a community Gatus dashboard on Grafana Labs (dashboard ID 24379) that gives you a ready-made view. Import it into your Grafana instance and point it at the Prometheus data source where Gatus metrics are stored.

This setup adds to the built-in status page rather than replacing it. The Gatus UI is for the public, a quick glance at whether services are up. The Grafana dashboard is for operators who want past trends, links to other metrics, and custom alert rules through Prometheus Alertmanager.

Practical Tips and Maintenance

A few things worth knowing after you have Gatus running.

Gatus has no built-in maintenance window feature. The simplest fix is to set enabled: false on an endpoint in the config file during planned downtime, then turn it back on afterward. If your config is in Git, that is just a commit and a container restart. You can automate it with a CI pipeline that edits the YAML and restarts the container on a schedule.

The [CERTIFICATE_EXPIRATION] condition checks the leaf certificate only. If you need to watch intermediate certificate expiry or full chain validation, you will need a separate tool or script.

Gatus checks the YAML config on startup and logs errors clearly. Get into the habit of running docker logs gatus after any config change. A broken YAML file stops Gatus from starting, which beats it running quietly with a bad config.

At 50 endpoints with 60-second intervals, expect about 40 MB of RAM use. Scaling to 200+ endpoints or using very short intervals (10-15 seconds) raises resource use. But Gatus stays light next to most monitoring stacks. It runs fine on a Raspberry Pi 4 or any low-end VPS.

If you use SQLite storage, back up the /data/gatus.db file often. The check history it holds powers the uptime percentage displays on the status page. Lose it and you start over with no past data.

Gatus fills a specific role well. It monitors your endpoints, tells you when something is wrong, and shows a status page to anyone who asks. It does not try to be a full observability platform. Paired with Prometheus and Grafana for metrics, and a log tool like Loki for logs, it forms one piece of a practical self-hosted monitoring stack that costs nothing and runs on hardware you already own.