Gatus on Ubuntu: Monitoring as Code with Docker

Mascote LinuxPro acompanhando um painel de monitoramento do Gatus

An endpoint can remain “up” for the server and still fail for the people using the product. The Gatus is a monitoring self-hosted solution: you describe in YAML what needs to be verified, it runs the probes, maintains a status page, and can alert when a condition is no longer true. In this first article, the goal is simple: run Gatus on Ubuntu with Docker, persist the history in SQLite, and create the first check. The more advanced options are left for a next guide.

What is Gatus

Gatus is an open source project under Apache-2.0 license, developed in Go. It verifies services via HTTP, ICMP, TCP and DNS and is not limited to “did the URL respond?”: conditions can validate HTTP code, response time, returned body, certificate expiration and other probe values. The result appears in a dashboard/status page and can feed alerts.

The central point is configuration as code. Instead of registering checks only in an interface, the endpoints live in YAML. This makes pull request review, Git history and repeating the same policy across environments easy. If your preference is an interface for creating monitors manually, read our guide on Uptime Kuma on Linux.

Go: why the language matters

The Gatus code is written in Go. In practice, this explains why it can be distributed as a single executable and why the official image does a Go compilation before starting a binary in the container. For those operating the service with Docker, there's no Go runtime to install on Ubuntu: the image already delivers the application. For those choosing non-container installation, the project itself documents go install github.com/TwiN/gatus/v5@latest.

This is not an automatic guarantee of low consumption in any scenario: number of endpoints, probe interval, history retention, and chosen database continue defining the load. But the absence of a language VM in the production process makes the operational model straightforward.

Why it finds failures before the user

Metrics from an application can appear normal when no clients are reaching it. If a balancer, DNS, or external route fails, there might not even be traffic to generate an error. Gatus makes an active request at the configured interval and evaluates the response. This way, it can alert that the public flow is broken before the first call arrives.

It is not a substitute for metrics, logs, and tracing. For CPU, memory, disk, and Linux server investigation, complement the strategy with Prometheus and Node Exporter. Think of Gatus as a synthetic availability and contract check: “does this important endpoint return what it promised?”.

Install Docker, Compose, and Nginx on Ubuntu

On a fresh Ubuntu, install Docker from the official repository and Nginx via APT. If the machine already runs containers, review installed packages before swapping docker.io, containerd or runc: mixing sources can cause conflict.

sudo apt update
sudo apt install -y ca-certificates curl nginx
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker nginx
sudo docker run hello-world
docker compose version

The test hello-world confirms that the daemon is running. Keep sudo in Docker commands or treat the entry in the group docker as administrative access to the host.

Before installing

This guide uses Docker Engine and the Docker Compose plugin on Ubuntu. If you don't have them yet, follow the installation from the official Docker repository in our article about Uptime Kuma. We will use the port 8080 linked only to 127.0.0.1: this is safe for the first test on the server and avoids publishing the panel without HTTPS and access control.

The official README also documents the installation via Go with go install github.com/TwiN/gatus/v5@latest. We will deliberately use Docker in this post to keep the installation short and reproducible. The Go installation, reverse proxy, authentication, and external databases deserve a separate tutorial.

Launch Gatus with Docker Compose

Create the directories. The directory config stores the versionable definition of the checks; data will receive SQLite, which preserves the history after restarts.

sudo install -d -m 0755 /opt/gatus/config /opt/gatus/data
sudo chown -R "$USER":"$USER" /opt/gatus
cd /opt/gatus

Create compose.yaml:

services:
  gatus:
    image: ghcr.io/twin/gatus:stable
    container_name: gatus
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      GATUS_CONFIG_PATH: /config/config.yaml
    volumes:
      - ./config:/config:ro
      - ./data:/data

The tag stable is the one indicated by the official quickstart, but it follows changes. For production, test a release and pin a known version tag when your policy requires predictability.

Write the first monitor

Create /opt/gatus/config/config.yaml. This example monitors the homepage and requires HTTP 200, response in less than two seconds and valid certificate for more than seven days:

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

endpoints:
  - name: LinuxPro
    group: site-publico
    url: "https://www.linuxpro.com.br/"
    interval: 5m
    conditions:
      - "[STATUS] == 200"
      - "[RESPONSE_TIME] < 2000"
      - "[CERTIFICATE_EXPIRATION] > 168h"

Change URL, name, and limits for your service. A two-second limit may not make sense for every application; the goal is to turn a real expectation into a condition. For a JSON API, Gatus also allows you to validate a body field, for example [BODY].status == UP, when that contract exists.

Database choice: memory, SQLite or PostgreSQL

Gatus supports three storage types: memory, sqlite and postgres. MySQL/MariaDB is not a supported backend by Gatus at the moment; it won't work to spin up a MySQL container and point the configuration to it.

  • Memory: is the default and is only for demonstration. All history is lost when the process restarts.
  • SQLite: is the best choice for a single instance, small or medium endpoint sets, and simple operation. This guide's Compose already uses it; keep ./data on local disk and back up the file data.db.
  • PostgreSQL: choose when the history grows, when there's already team-managed PostgreSQL, or when you need to separate the database from the application container. For large dashboards, Gatus offers storage.caching: true in SQLite and PostgreSQL.

The initial Compose in this article is the SQLite scenario: the volume ./data:/data and storage.type: sqlite in YAML is the “docker-compose for SQLite”. Do not remove these two parts, or you go back to in-memory storage.

Docker Compose with PostgreSQL

When SQLite is no longer sufficient, use an isolated PostgreSQL database in the same Docker network. Create a file .env with a strong password and do not send it to Git:

POSTGRES_DB=gatus
POSTGRES_USER=gatus
POSTGRES_PASSWORD=troque-por-uma-senha-longa

Then replace the compose.yaml with the example below. PostgreSQL does not publish a port on the host: only the Gatus container communicates with it.

services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    env_file: .env
    volumes:
      - postgres-data:/var/lib/postgresql
    networks: [gatus-internal]

  gatus:
    image: ghcr.io/twin/gatus:stable
    container_name: gatus
    restart: unless-stopped
    depends_on: [postgres]
    env_file: .env
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      GATUS_CONFIG_PATH: /config/config.yaml
    volumes:
      - ./config:/config:ro
    networks: [gatus-internal]

volumes:
  postgres-data:

networks:
  gatus-internal:

Also change the section storage from config/config.yaml. If the password has URL-reserved characters, it needs to be encoded for use in the connection URL.

storage:
  type: postgres
  path: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
  caching: true

The sslmode=disable is appropriate only for this internal Docker network. For PostgreSQL outside the host or managed, require TLS and use the connection URL provided by the service.

Start and validate

cd /opt/gatus
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100
curl -I http://127.0.0.1:8080

The curl confirms the service is listening locally. To open the panel without exposing the port, create an SSH tunnel from your machine:

ssh -L 8080:127.0.0.1:8080 usuario@seu-servidor

Then access http://localhost:8080 in the local browser. Do not open 8080 in the firewall just to make things easier: when you decide to expose a status page or administration, publish it behind HTTPS, authentication, and a reverse proxy configured for your environment.

Publish the dashboard with Nginx

Compose keeps Gatus at 127.0.0.1:8080. To access it via a domain, put Nginx in front and use HTTPS. In /etc/nginx/sites-available/gatus, adapt the domain and certificate paths:

server {
    listen 80;
    server_name status.exemplo.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name status.exemplo.com;

    ssl_certificate /etc/letsencrypt/live/status.exemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/status.exemplo.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;
    }
}
sudo ln -s /etc/nginx/sites-available/gatus /etc/nginx/sites-enabled/gatus
sudo nginx -t
sudo systemctl reload nginx

Issue the certificate before enabling the HTTPS block and do not expose the administrative dashboard without protection. Gatus offers Basic Auth and OIDC in its own configuration; use one of them when the page is not public. For Nginx implementation on Ubuntu, also refer to our guide on Nginx and various PHP versions.

Gatus and AI: a practical use, without hype

Gatus is not an AI tool and does not “fix” AI-generated code. The utility here is different: changes become more frequent when teams use AI assistance, and a synthetic probe can verify the critical path after deployment. In addition to testing 200, validate a JSON field, an expected response, or a time limit. The project itself discusses this prevention role in the context of changes accelerated by AI.

Don't turn a probe into a destructive action. If creating checks that make POST requests to test a flow, the operation must be idempotent, isolated, and cannot alter real production data. YAML is an operational contract: review it like you'd review code.

What's left for the next step

After the first endpoint is healthy, advance step by step: add DNS or TCP, configure alerts, use a separate public page, and put the YAML in Git. Before storing webhook tokens or passwords in configuration, remove them from the repository and use the secret management mechanism adopted by your team.

For complete details on conditions, alerts, storage, and security, see the Gatus documentation and the official repository. Also follow the releases before updating the container image. With a short configuration file and a well-thought-out check, Gatus already prevents the user from being your first monitor.

Related reading: How Gatus Can Help Prevent Disasters in the Era of AI, on the project's official blog.