A lightweight, self-hosted service health monitor with an intelligent keep-alive mechanism for free-tier hosting (Render, Railway, Fly.io). Prevents application sleep by sending configurable periodic health checks, controlled by a GitHub Actions cron schedule with Business Hours support.
- Real-time dashboard — live status for all monitored services with response time and uptime tracking
- Keep-alive engine — 3-phase cold-start strategy (fire → wait → verify) optimised for Render free tier
- Business Hours — restrict wake-up calls to configured time windows (CET/CEST, overnight windows supported)
- GitHub Actions integration — cron-driven wake mechanism that survives container restarts; settings persist via GitHub Variables
- Manual check — trigger a full check from the dashboard, bypassing business hours
- Auto-ping fallback — APScheduler-based local fallback for development or self-hosted deployments
- SSRF protection — blocks requests to localhost, link-local addresses, and cloud metadata endpoints
- Infrastructure as Code — Docker Compose for local deployment, Ansible playbook for server provisioning
- CI/CD pipeline — GitHub Actions: lint → unit tests → Docker build → integration tests → Docker Compose test
GitHub Actions cron (every 12 min, shifted)
│
├─ Check business hours (GitHub Variables: BH_ENABLED / BH_START / BH_END)
│
├─ [Phase 1] Fire: GET all service URLs simultaneously → kicks cold starts
│
├─ [Phase 2] Wait: 90 s — allows Render instances to boot
│
└─ [Phase 3] Verify: GET each URL, update Pulse state via /api/wake-and-check
APScheduler runs scheduled_check() on the configured interval. Business Hours are respected identically to the GitHub Actions logic.
| Layer | Technology |
|---|---|
| Backend | Python 3.12, Flask 3.1 |
| Server | Gunicorn 23 |
| Scheduler | APScheduler 3.10 |
| HTTP client | Requests 2.32 |
| Frontend | Vanilla JS, HTML/CSS (no framework) |
| Containerisation | Docker, Docker Compose |
| Provisioning | Ansible |
| CI/CD | GitHub Actions |
| Hosting | Render (free tier) |
git clone https://github.com/Shellty-IT/Shellty-Pulse.git
cd Shellty-PulseEdit the SERVICES variable in docker-compose.yml, then:
docker compose up --buildDashboard: http://localhost:5000
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # edit SERVICES and optional vars
python app.pyAll configuration is provided via environment variables.
| Variable | Default | Description |
|---|---|---|
PORT |
5000 |
HTTP port |
PING_INTERVAL |
900 |
Auto-ping interval in seconds (local fallback) |
REQUEST_TIMEOUT |
90 |
Per-request timeout in seconds |
MAX_SERVICES |
50 |
Maximum number of monitored services |
SERVICES |
[] |
JSON array of services to preload on startup |
GITHUB_TOKEN |
— | Personal access token with repo scope (Variables read/write) |
GITHUB_REPO |
— | Repository in owner/repo format |
WAKE_SECRET |
— | Shared secret for /api/wake-and-check (optional but recommended) |
[
{
"name": "My API",
"url": "https://my-api.onrender.com/health",
"frontend_url": "https://my-app.com"
}
]frontend_url is optional — when provided, the service name on the dashboard becomes a clickable link.
600 (10 min) · 900 (15 min) · 1800 (30 min) · 3600 (1 h) · 86400 (24 h) · 172800 (48 h)
Business Hours restrict GitHub Actions wake-up calls to a configured time window (Europe/Warsaw timezone). Outside this window services are allowed to sleep, conserving Render free-tier hours.
Settings are stored as GitHub Actions Variables so they survive container restarts on Render:
| Variable | Example | Description |
|---|---|---|
BH_ENABLED |
true |
Enable/disable business hours |
BH_START |
9 |
Start hour (0–23, inclusive) |
BH_END |
15 |
End hour (0–23, exclusive + 15 min buffer) |
Overnight windows are supported — set BH_START > BH_END (e.g. 23 → 1 means 23:00–01:15).
Settings can be changed live from the dashboard and are synced back to GitHub Variables automatically.
Go to Settings → Secrets and variables → Actions → Secrets:
| Secret | Value |
|---|---|
WAKE_SECRET |
Random string matching the WAKE_SECRET env var on Render |
Go to Settings → Secrets and variables → Actions → Variables:
| Variable | Default |
|---|---|
BH_ENABLED |
false |
BH_START |
9 |
BH_END |
15 |
Variables are managed automatically by the dashboard after the first manual save.
Edit .github/workflows/wake-shellty-pulse.yml and update:
env:
APP_URL: https://your-app.onrender.com- Connect your GitHub repository in the Render dashboard
- Set environment variables:
SERVICES,GITHUB_TOKEN,GITHUB_REPO,WAKE_SECRET - Deploy — GitHub Actions will handle scheduled wake-ups automatically
Requires Ubuntu 20.04 / 22.04 / 24.04, sudo access, and the community.docker collection:
ansible-galaxy collection install community.docker
# Local deployment
ansible-playbook ansible/playbook.yml -i "localhost," -c local
# Remote server
ansible-playbook ansible/playbook.yml -i inventory.iniThe playbook installs Docker CE, builds the image, and starts the container with a health check.
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Application health check (version, uptime, scheduler state) |
GET |
/ |
Dashboard |
GET |
/api/services |
List all services with status and meta |
POST |
/api/services |
Add a service {name, url, frontend_url?} |
DELETE |
/api/services/<id> |
Remove a service |
POST |
/api/services/<id>/check |
Manual single-service check |
POST |
/api/check-all |
Legacy: check all services locally |
POST |
/api/fire-all |
Phase 1: fire all services (cold-start kick) |
POST |
/api/verify-all |
Phase 3: verify all services (call 120 s after fire) |
POST |
/api/wake-and-check |
Called by GitHub Actions — requires X-Wake-Secret header if configured |
POST |
/api/toggle-auto-ping |
Toggle local APScheduler on/off |
POST |
/api/ping-interval |
Set interval {interval: <seconds>} |
POST |
/api/trigger-manual-check |
Trigger GitHub Actions workflow from dashboard |
POST |
/api/business-hours |
Configure BH {enabled, start, end} — syncs to GitHub Variables |
| Status | Condition |
|---|---|
| Operational | HTTP 200, response time < 1 s |
| Degraded | HTTP 200, response time 1–3 s |
| Slow | HTTP 200, response time > 3 s |
| Down | Non-200 response, timeout, or connection error |
| Unknown | Service has not been checked yet |
The pipeline runs on every push and pull request to main:
lint
└─ py_compile + flake8 + unit tests (factory, SSRF, validation)
│
├─ build-and-test
│ └─ Docker build → container start → health check → API integration tests
│
└─ compose-test
└─ docker compose up → health check → service preload verification
Shellty-Pulse/
├── app.py # Application entry point
├── startup.sh # Gunicorn start + service preload
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── ansible/
│ └── playbook.yml # VPS provisioning
├── .github/
│ └── workflows/
│ ├── ci.yml # CI pipeline
│ └── wake-shellty-pulse.yml # Scheduled keep-alive
└── pulse/
├── config.py # All constants and env vars
├── state.py # In-memory application state
├── models.py # Service model and status logic
├── checker.py # Health-check engine (fire / verify)
├── scheduler.py # APScheduler lifecycle + GitHub Variables sync
├── validators.py # Input validation and SSRF protection
├── routes/
│ ├── api.py # REST API blueprint (/api/*)
│ └── dashboard.py # Dashboard route (/)
└── templates/
└── dashboard.html # Single-page dashboard
- SSRF protection — rejects URLs resolving to
localhost,127.0.0.1,0.0.0.0,169.254.169.254, andmetadata.google.internal - Wake secret —
/api/wake-and-checkvalidatesX-Wake-Secretheader whenWAKE_SECRETenv var is set - Non-root container — application runs as a dedicated
pulseuser - Read-only filesystem — Docker Compose mounts container filesystem as read-only (
tmpfsfor/tmp) - No new privileges —
security_opt: no-new-privileges:true - Input validation — name and URL length limits enforced on all service endpoints
MIT
Built by Shellty IT