Gatus is a lightweight health checker written in Go. You describe endpoints and the conditions they must meet in a single YAML file, and Gatus probes them on a schedule, keeps the history, sends alerts when checks fail, and serves a status page. In this tutorial you will run Gatus with Docker Compose on Ubuntu 24.04, monitor HTTP, TCP, DNS and TLS certificate health, send failure alerts to Slack, and publish the status page at https://status.your_domain behind Nginx.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Gatus needs very little: 1 vCPU and 512 MB of RAM are enough for dozens of endpoints.
  • A non-root user with sudo privileges and UFW enabled with SSH allowed.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository.
  • A domain name with a DNS A record for status.your_domain pointing to your_server_ip.
  • A Slack incoming webhook URL, if you want the Slack alerts from Step 5.

Step 1 - Creating the project directory

Keep the Compose file, the Gatus configuration and the secrets together in one directory:

sudo mkdir -p /opt/gatus/config
sudo chown -R "$USER": /opt/gatus
cd /opt/gatus

Gatus reads its configuration from /config/config.yaml inside the container. Mounting the whole config directory, rather than the single file, means edits made with any editor are always visible to the container.

Step 2 - Writing a first configuration

Start with one HTTP check so you can confirm the stack works before adding more. Create the configuration file:

nano /opt/gatus/config/config.yaml

Add the following, replacing your_domain with a site you want to monitor:

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

endpoints:
  - name: Website
    group: public
    url: "https://your_domain"
    interval: 1m
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 800"

storage makes Gatus keep results in SQLite, so uptime history survives restarts; without it everything lives in memory. Each condition is an expression that must be true for the check to pass. [STATUS] is the HTTP status code and [RESPONSE_TIME] is measured in milliseconds.

Step 3 - Running Gatus with Docker Compose

Create the Compose file:

nano /opt/gatus/docker-compose.yml
services:
  gatus:
    image: ghcr.io/twin/gatus:stable
    container_name: gatus
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    env_file: .env
    volumes:
      - ./config:/config:ro
      - gatus-data:/data

volumes:
  gatus-data:

The port is bound to 127.0.0.1 only. Docker writes its own iptables rules for published ports and bypasses UFW, so binding to localhost is what actually keeps Gatus off the public interface; Nginx will be the only way in. The named volume gatus-data holds the SQLite database.

env_file points to a file that will hold secrets such as the Slack webhook. Create it empty for now and restrict it:

touch /opt/gatus/.env
chmod 600 /opt/gatus/.env

Start Gatus:

docker compose up -d

Check that it is healthy:

curl -s http://127.0.0.1:8080/health
{"status":"UP"}

After a minute the first check has run. Query the API to see its result:

curl -s http://127.0.0.1:8080/api/v1/endpoints/statuses | head -c 400; echo

The output is a JSON array with one entry per endpoint, including a results list where "success":true means all conditions passed. If Gatus does not start, docker compose logs gatus shows YAML and validation errors with the offending key.

Step 4 - Adding TCP, DNS and certificate checks

Gatus decides the check type from the URL scheme. Open the configuration again:

nano /opt/gatus/config/config.yaml

Replace the endpoints section with this version, adjusting host names to your own services:

endpoints:
  - name: Website
    group: public
    url: "https://your_domain"
    interval: 1m
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 800"
      - "[CERTIFICATE_EXPIRATION] > 336h"

  - name: API health
    group: public
    url: "https://api.your_domain/health"
    interval: 30s
    conditions:
      - "[STATUS] == 200"
      - "[BODY].status == ok"

  - name: PostgreSQL
    group: internal
    url: "tcp://db.your_domain:5432"
    interval: 1m
    conditions:
      - "[CONNECTED] == true"

  - name: DNS resolution
    group: internal
    url: "1.1.1.1"
    interval: 5m
    dns:
      query-name: "your_domain"
      query-type: "A"
    conditions:
      - "[DNS_RCODE] == NOERROR"

What each check does:

  • [CERTIFICATE_EXPIRATION] > 336h fails when the TLS certificate expires in less than 14 days, which catches broken renewals early.
  • [BODY].status == ok parses the response as JSON and compares the status field. Use this for health endpoints that return something like {"status":"ok"}.
  • A tcp:// URL only checks that the port accepts connections; [CONNECTED] is the result.
  • For DNS checks, url is the resolver to query and the dns block says what to ask for.

Other useful placeholders are [IP] (the resolved address), len([BODY].items) for the length of a JSON array, and [BODY] == pat(*version*) for wildcard pattern matching.

Restart Gatus to apply the changes:

docker compose restart gatus

Confirm that the configuration loaded without errors:

docker compose logs --tail=20 gatus

Step 5 - Configuring Slack alerts

Alert providers are declared once in the top-level alerting section, and each endpoint opts in with an alerts list. Put the webhook URL in .env so it stays out of the configuration file:

nano /opt/gatus/.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your/webhook/path

Gatus expands environment variables written as ${NAME} in its configuration. Open config.yaml and add this section at the top level:

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

failure-threshold: 3 waits for three consecutive failures before alerting, which filters out single network blips. success-threshold: 2 requires two passing checks before sending the resolved message.

Then enable the alert on the endpoints that should page you, for example:

  - name: Website
    group: public
    url: "https://your_domain"
    interval: 1m
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 800"
      - "[CERTIFICATE_EXPIRATION] > 336h"
    alerts:
      - type: slack
        description: "Public website is failing"

Environment files are read when the container is created, so recreate it rather than restarting:

docker compose up -d --force-recreate

To test the alert path, temporarily add an endpoint that is guaranteed to fail:

  - name: Alert test
    group: internal
    url: "https://your_domain"
    interval: 30s
    conditions:
      - "[STATUS] == 418"
    alerts:
      - type: slack

Restart Gatus. After three failed checks (about a minute and a half) a triggered alert appears in your Slack channel. Remove the test endpoint and restart again; you will not get a resolved message because the endpoint no longer exists, which is expected.

Other providers such as email, telegram, pagerduty and custom (a generic webhook) use the same pattern: credentials under alerting.<provider> and type: <provider> on the endpoint.

Step 6 - Publishing the status page with Nginx and HTTPS

Install Nginx and Certbot:

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx

Create a server block for the status page:

sudo nano /etc/nginx/sites-available/gatus
server {
    listen 80;
    listen [::]:80;
    server_name status.your_domain;

    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;
    }
}

Enable it, test the syntax and reload:

sudo ln -s /etc/nginx/sites-available/gatus /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Allow HTTP and HTTPS through UFW, then request a certificate. Certbot edits the server block to add TLS and an HTTP to HTTPS redirect:

sudo ufw allow 'Nginx Full'
sudo certbot --nginx -d status.your_domain

Open https://status.your_domain. You will see the endpoints grouped by their group value, with a history bar and response times for each one.

To brand the page, add a ui section to config.yaml and restart Gatus:

ui:
  title: "Example Inc. status"
  header: "Example Inc. status"
  description: "Current state of Example Inc. services"
  link: "https://your_domain"

Troubleshooting

An endpoint fails although it works in the browser. Run the same request from the server with curl -sv https://your_domain -o /dev/null to see the status code and timing Gatus sees. Response time conditions that are too tight and JSON conditions that do not match the exact body are the usual causes; the API output from Step 3 shows which condition failed.

Slack alerts never arrive. Check that the variable reached the container with docker compose exec gatus env | grep SLACK; if it is missing, recreate the container with docker compose up -d --force-recreate. Remember that nothing is sent until failure-threshold consecutive failures have happened.

The page shows no history after a restart. Make sure the storage block is present and that the gatus-data volume is mounted at /data.

Conclusion

You now have Gatus checking HTTP, TCP, DNS and certificate health, alerting Slack after repeated failures and serving a public status page over HTTPS. Because the whole setup is one YAML file, keep config.yaml in version control and review changes like code. From here you can add an email or pagerduty provider for critical endpoints, run a second Gatus instance in another location to monitor the first, or embed the uptime and health badges that the Gatus API generates in your project READMEs.