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
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) }}
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"
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
shields.io endpoint badge data sources api/{slug}/response-time.json, uptime.json:
{"schemaVersion":1,"label":"response time","message":"739 ms","color":"yellow"}
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
-->
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
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:
- Workflows triggered by the default token won't trigger downstream workflows (to prevent loops), breaking the "probe → create Issue → notify" chain in the middle.
- 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
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
- Upptime official repository
- shields.io endpoint badge documentation
- GitHub Actions scheduled task documentation
- HagiCode status page example
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.
- Author: newbe36524
- Original URL: https://docs.hagicode.com/go?platform=devto&target=%2Fblog%2F2026-06-18-how-to-use-upptime-for-free-status-page%2F
- License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.
Top comments (0)