DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

How to Use Upptime to Build Your Own Status Page for Free

How to Use Upptime to Build Your Own Status Page for Free

Move monitoring entirely into a GitHub repo—Actions as probes, the repo as a database, Pages as a CDN, and Issues as an event log. Zero servers, zero monthly fees, yet somehow you end up with a status page that you can view, query, and trace. Call it black magic or the wisdom of the frugal—either way, it runs.

Background

When operating a small product matrix consisting of over a dozen external services, "is it actually working" became a common refrain. Customers report they can't access it, you SSH in and curl once to find it's fine; a few minutes later it goes down, but this time you weren't watching. Commercial monitoring (Pingdom, UptimeRobot's premium tier, Datadog) can certainly solve this, but they charge either by site or by request count—for an indie developer, neither cost nor mental burden is quite worth it.

More importantly, users need to be able to check the status page themselves. Ideally, one domain (say status.hagicode.com) would display real-time availability for each service, response time curves, historical events, and automatically log incidents when failures occur with automatic notifications. The traditional approach requires four pieces—a server running cron, a database storing historical data, a frontend site, and a CDN. Once you lay out these four, operational costs immediately outweigh the services being monitored—using a sledgehammer to crack a nut, and even the nut feels crowded.

To address these pain points, we made a decision: move the entire monitoring solution directly to GitHub. This decision brought changes larger than you might imagine—I'll get to that gradually.

About HagiCode

The solution shared in this article comes from our experience building HagiCode. HagiCode is an AI code assistant project that exposes over a dozen public services including web pages, documentation sites, and download endpoints, driven by the main repository HagiCode-org/site. These sites must remain stable and available, so status monitoring is not optional for us—it's a necessity. The Upptime solution below is exactly what HagiCode's production environment uses—I didn't make this up.

Analysis: How Upptime Actually Works

Upptime is essentially a GitHub repository template plus six workflows generated by that template. The key to understanding it is seeing clearly "who calls whom, when, and produces what that lands where." Break it apart and it's not so mysterious.

Data Flow: One Configuration File Drives Everything

The entire system revolves around a single declarative configuration file .upptimerc.yml. HagiCode's actual configuration structure looks roughly like this:

owner: HagiCode-org
repo: upptime

sites:
  - name: HagiCode Website
    url: https://hagicode.com
  - name: HagiCode Docs
    url: https://docs.hagicode.com
  - name: Server Package Index
    url: https://index.hagicode.com/server/index.json
  # ... 14 sites total

status-website:
  cname: status.hagicode.com
  logoUrl: https://raw.githubusercontent.com/HagiCode-org/upptime/master/assets/upptime-icon.svg
  name: HagiCode Status
  introTitle: "**HagiCode Status**"
  introMessage: Real-time availability tracking for public HagiCode websites and download endpoints.
  navbar:
    - title: Status
      href: /
    - title: GitHub
      href: https://github.com/$OWNER/$REPO
Enter fullscreen mode Exit fullscreen mode

Two points here are worth calling out. First, sites can monitor both web pages (returning HTML) and pure JSON endpoints (like index.json)—Upptime only cares about HTTP status codes and response time, not content validation. Second, cname points to status.hagicode.com, which requires you to own that domain and configure DNS to point to GitHub Pages—after all, freebies aside, you still need to provide your own domain.

The Division of Six Workflows

All files under .github/workflows/ have a warning at the top Do not edit this file directly!—they're auto-updated weekly by the template; you only need to modify .upptimerc.yml. Each workflow is triggered by cron, calling different subcommands of the same action upptime/uptime-monitor@v1.42.6 with clear division of labor, which is actually quite reassuring:

Workflow cron command purpose
uptime.yml */5 * * * * update probe every 5 minutes, write history/*.yml
response-time.yml response-time calculate response time statistics
graphs.yml graphs generate day/week/month/year PNG curves
summary.yml summary update status table in README
site.yml 0 1 * * * site build static site daily, deploy to Pages
update-template.yml 0 0 * * * sync upstream template weekly

The core snippet of uptime.yml shows how the "probe" runs:

on:
  schedule:
    - cron: "*/5 * * * *"
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GH_PAT || github.token }}
      - name: Check endpoint status
        uses: upptime/uptime-monitor@v1.42.6
        with:
          command: "update"
        env:
          GH_PAT: ${{ secrets.GH_PAT || github.token }}
          SECRETS_CONTEXT: ${{ toJson(secrets) }}
Enter fullscreen mode Exit fullscreen mode

site.yml adds one more step, using peaceiris/actions-gh-pages@v4 to push build artifacts to the gh-pages branch:

- uses: peaceiris/actions-gh-pages@v4
  with:
    github_token: ${{ secrets.GH_PAT || github.token }}
    publish_dir: "site/status-page/__sapper__/export/"
    user_name: "Upptime Bot"
    user_email: "73812536+upptime-bot@users.noreply.github.com"
Enter fullscreen mode Exit fullscreen mode

Data Persistence: Files as Database

Monitoring results aren't stored in a database; instead they're committed back to the repository as files. This sounds a bit wild, but it's actually solid in practice. Each site produces three types of artifacts.

Status snapshot history/{slug}.yml, for example history/hagi-code-website.yml:

url: https://hagicode.com
status: up
code: 200
responseTime: 96
lastUpdated: 2026-06-17T00:22:34.485Z
startTime: 2026-03-24T10:07:32.531Z
Enter fullscreen mode Exit fullscreen mode

shields.io endpoint badge data sources api/{slug}/response-time.json, uptime.json:

{"schemaVersion":1,"label":"response time","message":"739 ms","color":"yellow"}
Enter fullscreen mode Exit fullscreen mode

And response time curve graphs graphs/{slug}/response-time-{day,week,month,year}.png.

The trade-off of this "files as database" approach is well-considered: write-heavy, read-light, controllable scale (about 288 samples per day per site, storing increments rather than full logs), naturally versioned, zero infrastructure. The cost, of course, is that the repository grows continuously and you occasionally need to look back and care about it.

Events and Notifications: Issues as Event Log

Incident logging relies on GitHub Issues, paired with two built-in repository templates: .github/ISSUE_TEMPLATE/bug_report.md (user-reported issues) and maintainance-event.md (scheduled maintenance). The maintenance template uses frontmatter to express the time window:

<!--
start: 2021-08-24T13:00:00.220Z
end: 2021-08-24T14:00:00.220Z
expectedDown: google, hacker-news
-->
Enter fullscreen mode Exit fullscreen mode

Upptime parses these Issues and renders "maintenance in progress" and "past events" to the status page and README. Notifications rely on Issues' own watch mechanism, plus configurable webhooks, Slack, Telegram (declare notifications at the top of .upptimerc.yml; HagiCode's example repo currently doesn't have this enabled—after all, one less thing is one less thing).

Solution: Five Steps to Replicate a Status Page

Replicating a HagiCode-style status page, from zero to live, takes five steps total. Five steps sounds like a lot, but each one is short—take your time.

Step 1: Create Repository from Template

Don't git clone and modify—use GitHub's "Use this template" to create a repository directly (e.g., your-org/upptime). The template already includes all workflows, Issue templates, and the static site skeleton. After cloning locally, the only thing you need to manually modify is .upptimerc.yml—leave everything else alone.

Step 2: Edit .upptimerc.yml

Change owner/repo to yours, list the addresses you want to monitor in sites, configure status-website for the site. The minimum viable version looks roughly like this:

owner: your-org
repo: upptime

sites:
  - name: Main Site
    url: https://example.com
  - name: API Health
    url: https://api.example.com/health
    expectedStatusCodes:
      - 200

status-website:
  cname: status.example.com   # delete if no domain, use default your-org.github.io/upptime
  name: Example Status
  introTitle: "**Example Status**"
  introMessage: 服务可用性实时监控
  navbar:
    - title: Status
      href: /
    - title: GitHub
      href: https://github.com/$OWNER/$REPO
Enter fullscreen mode Exit fullscreen mode

Advanced items: expectedStatusCodes limits acceptable status codes (default 200-399); headers customizes request headers (for endpoints requiring auth); maxResponseTime marks slow responses. Use these as needed—take what you need.

Step 3: Configure Secrets and Permissions

The workflow defaults to ${{ secrets.GH_PAT || github.token }}. github.token can run the basic flow, but two limitations will bite you:

  1. Workflows triggered by the default token won't trigger downstream workflows (to prevent loops), breaking the "probe → create Issue → notify" chain in the middle.
  2. Insufficient permissions for cross-repo operations (like multi-org).

Recommend creating a new PAT (requires repo + workflow permissions) and storing it as repository Secret GH_PAT. update-template.yml has a specific check: without GH_PAT, it skips template auto-update and prints a warning, so this secret isn't just optional—it's key to peace of mind.

Step 4: Enable GitHub Pages

Repository Settings → Pages → Source select Deploy from a branch, branch select gh-pages, directory /root. site.yml automatically pushes build artifacts to this branch every day at 1 AM. If you configured cname, add a CNAME record at your DNS provider pointing to your-org.github.io.

It's also good to manually trigger once the first time: Actions page find "Static Site CI" → Run workflow, no need to wait foolishly for scheduled tasks—seeing results one second earlier means peace of mind one second earlier.

Step 5: Verification and Maintenance

After pushing the config, go to Actions and check if "Uptime CI" runs every 5 minutes and if history/ starts showing *.yml files. The status page address is https://<your-org>.github.io/upptime/ or your custom domain. Later, adding sites or changing domains only requires touching .upptimerc.yml one file—workflows are fully automated. HagiCode has maintained availability for 14 endpoints using this mechanism for over a year now, basically worry-free.

Practice: I've Walked Through the Potholes for You

The following items are experience accumulated from HagiCode's actual operation—written down so you can take fewer detours.

Practice 1: Monitoring Granularity Selection

HagiCode puts web pages (https://hagicode.com) and pure data endpoints (https://index.hagicode.com/server/index.json) in the same sites list. For JSON endpoints, Upptime makes requests and parses HTTP status codes but doesn't validate content structure. If you need deep checks like "returns 200 but content is wrong," you'll need to supplement with expectedStatusCodes plus external probes—Upptime itself only does black-box HTTP checks—it reads the face, not the mind.

Practice 2: Clever Use of Response Time Badges

api/{slug}/response-time.json is a shields.io endpoint badge data source. HagiCode's README heavily references these URLs:

https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FHagiCode-org%2Fupptime%2FHEAD%2Fapi%2Fhagi-code-website%2Fresponse-time.json
Enter fullscreen mode Exit fullscreen mode

This way, you can embed real-time response time badges in any Markdown (project README, blog, third-party pages), with colors driven by the values in message and the color field. Note that using HEAD rather than master/main to reference raw files avoids widespread failures after branch renames—details hide stability.

Practice 3: Repository Size Control

Sampling every 5 minutes, history/ accumulates considerable volume over a year. Upptime uses incremental YAML rather than full logs, which is relatively restrained, but it's still recommended to occasionally check repository size. If a site's monitoring value declines, remove it from sites—corresponding historical files can also be manually cleaned up; after all, if you can't bear to delete, the repository will eventually show you what bloating means.

Practice 4: Real Usage of Maintenance Events

maintainance-event.md isn't decoration. Before a planned release, create an Issue using the template, fill in start/end/expectedDown, and Upptime will mark corresponding sites during that period as "scheduled maintenance," not counting them toward availability statistics, avoiding dragging down the full-year SLA with a normal release. HagiCode's expectedDown supports comma-separated site name lists, corresponding one-to-one with sites[].name.

Practice 5: Boundaries Between Template Updates and Customization

The Do not edit this file directly! at the top of all .github/workflows/*.yml isn't meant to scare you. update-template.yml overwrites these files with the upstream template weekly. When you need custom behavior, the correct approach is to use officially supported config items in .upptimerc.yml (like skipTopics, customStatusWebsite, runnerSettings), not to modify workflows. If you really must modify workflows, either turn off update-template.yml or fork and maintain the template yourself—the latter loses painless upgrades; weigh the trade-offs yourself.

Practice 6: Reality Constraints of Free Quotas

GitHub Actions is free and unlimited for public repositories, which Upptime's design exploits. Private repositories have 2000 free minutes per month, while uptime.yml runs every 5 minutes for about 1 minute each time—just this one item costs about 8640 minutes per month, exceeding the quota. So the Upptime repository must be public—this is the premise of "free"—don't go private for secrecy and then receive a bill, that's awkward.

Conclusion

Returning to the initial question: monitoring a pile of external services—is there really a cheap solution? HagiCode's answer is—yes, and so cheap you'll doubt it's real. Upptime breaks monitoring into four GitHub native components:

  • Probes = GitHub Actions cron
  • Database = YAML/JSON files in the repository
  • CDN = GitHub Pages
  • Event log = GitHub Issues

You get: real-time availability, response time curves, historical events, availability badges, custom domains, automatic notifications—all with zero servers and zero monthly fees. The cost is keeping the repository public and occasionally caring about repository size. Compared to hand-rolling a monitoring solution, this cost is actually far lighter.

The reason this solution works is behind GitHub's ecosystem's sincere subsidy for open-source projects. If you're also maintaining a small multi-site product matrix, I strongly recommend spending an afternoon setting this up—it's far less worry than hand-rolling monitoring.

References

Summary

Around "How to use Upptime to build your own status page for free," a more prudent approach is to gradually get key configurations, dependency boundaries, and landing paths working first, then fill in optimization details.

Once goals, steps, and acceptance criteria are clear, such solutions can typically enter actual delivery more smoothly.

Original Article & License

Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.

Top comments (0)