الملفات
GHyamah-Test/q1-deploy-monitor

Q1 — Deploy & Monitor an API on Ghaymah

A minimal FastAPI service, containerised and deployed on the Ghaymah container platform, plus a polling monitor and a static dashboard that visualises its availability, response time and request count.

q1-deploy-monitor/
├── app/
│   ├── main.py            # FastAPI app: /, /health, /metrics
│   ├── requirements.txt   # pinned fastapi + uvicorn
│   ├── Dockerfile         # python:3.12-slim, non-root, HEALTHCHECK
│   └── .dockerignore
├── monitor/
│   ├── monitor.py         # polls /health + /metrics every 30s (stdlib only)
│   └── data/checks.json   # created on first run — the dashboard's data source
├── dashboard/
│   └── index.html         # single self-contained page (Chart.js from CDN)
└── README.md

1. The API

Method Path Response
GET / service name, version, release, endpoint list, start time
GET /health {"status":"ok","release":"<git-commit-sha>","uptime_s":12.34,"timestamp":"2026-07-26T20:46:06.847603+00:00"} — HTTP 200
GET /metrics {"requests_total":42,"started_at":"<iso8601>"}
GET /docs interactive OpenAPI docs (FastAPI built-in)

Example / response:

{
  "service": "ghaymah-api",
  "version": "1.0.0",
  "release": "60b9e2c38e3f1a509383c6d61a7f76b9d043214a",
  "message": "Ghaymah deployment demo API",
  "endpoints": ["/", "/health", "/metrics", "/docs"],
  "started_at": "2026-07-26T20:46:03.315313+00:00",
  "timestamp": "2026-07-26T20:52:18.907569+00:00"
}

/health performs no downstream checks (no DB, no network) — it reports process liveness only, so a failing check always means "restart me", which is exactly the signal an orchestrator's health probe should act on.

/metrics is backed by an in-memory counter incremented by an HTTP middleware on every request. It is per-process and deliberately resets on restart — a counter that drops to zero in the dashboard is a visible signal that the container was restarted or redeployed.

Environment variables (all optional): APP_NAME, APP_VERSION, RELEASE_SHA, and PORT (default 8080). CI embeds the immutable Git commit in RELEASE_SHA, and /health exposes it as release for deployment verification.

Run locally without Docker

cd q1-deploy-monitor/app
python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python main.py            # or: uvicorn main:app --host 0.0.0.0 --port 8080
curl http://localhost:8080/health

2. Build and run the Docker image locally

docker build -t ghaymah-api:latest ./q1-deploy-monitor/app
docker run --rm -p 8080:8080 --name ghaymah-api ghaymah-api:latest

Verify:

curl -f http://localhost:8080/health && curl http://localhost:8080/metrics

Docker's own health probe (defined by HEALTHCHECK in the Dockerfile) shows up after ~10s in docker ps as (healthy):

docker ps --filter name=ghaymah-api

Image design notes (what the Dockerfile is doing and why):

  • python:3.12-slim — small base, no build toolchain in the final image.
  • requirements.txt is copied and installed before the application code, so editing main.py reuses the cached dependency layer instead of reinstalling FastAPI on every build.
  • Runs as the non-root user appuser (uid 10001).
  • curl is the only extra apt package, installed solely for the HEALTHCHECK; apt lists are removed in the same layer.
  • EXPOSE 8080 matches the port Ghaymah is configured with below.

3. Push the image and deploy on Ghaymah

Ghaymah deploys a container from a container image URL. This deployment uses Docker Hub namespace agamy74.

3.1 Push to Docker Hub

docker login
docker tag ghaymah-api:latest docker.io/agamy74/ghaymah-api:latest
docker push docker.io/agamy74/ghaymah-api:latest

Building on an Apple Silicon / ARM machine? Build for the platform Ghaymah runs (linux/amd64) or the container will fail to start:

docker buildx build --platform linux/amd64 -t docker.io/agamy74/ghaymah-api:latest --push ./q1-deploy-monitor/app

Make sure the Docker Hub repository is public — Ghaymah pulls the image anonymously from the URL you paste in.

3.2 Deploy on the Ghaymah dashboard

Field Value
Container Image URL docker.io/agamy74/ghaymah-api:<git-commit-sha>
Application Name ghaymah-api
Port Number 8080 (must match the EXPOSEd port)
Public Access enabled
Environment Variables (optional) APP_NAME=ghaymah-api, APP_VERSION=1.0.0

Then click Deploy. Once the deployment reports as running, Ghaymah assigns the service a public URL.

3.3 Verify the deployment

curl -f https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/health

Expected: HTTP 200 with {"status":"ok","release":"<git-commit-sha>","uptime_s":...,"timestamp":"..."}.

Also open https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/docs in a browser for the OpenAPI page, and screenshot both the running service in the Ghaymah dashboard and the /health response for the submission.

Redeploying a new version: push a new image tag and update the Container Image URL on the service. Prefer an explicit tag (e.g. :v2 or the git SHA) over :latest so a redeploy is unambiguous about which build is running.


4. Run the monitor

monitor/monitor.py uses the Python standard library only — nothing to install.

export APP_URL="https://ghaymah-api-615e99f13665.hosted.ghaymah.systems"
python q1-deploy-monitor/monitor/monitor.py

On Windows PowerShell:

$env:APP_URL="https://ghaymah-api-615e99f13665.hosted.ghaymah.systems"; python q1-deploy-monitor\monitor\monitor.py

Every 30 seconds it issues GET $APP_URL/health with a 5 s timeout, then GET $APP_URL/metrics, and appends one record to monitor/data/checks.json (a JSON array, created on first run):

The current /health response shape is:

{
  "status": "ok",
  "release": "60b9e2c38e3f1a509383c6d61a7f76b9d043214a",
  "uptime_s": 3.532,
  "timestamp": "2026-07-26T20:46:06.847603+00:00"
}

The monitor converts that response into the following derived history record. It intentionally stores status, HTTP code, latency, and request count rather than duplicating every response field:

{
  "ts": "2026-07-26T15:37:29.886772+00:00",
  "status": "up",
  "code": 200,
  "latency_ms": 65.3,
  "requests": 2
}
  • Any timeout, connection error, TLS failure or non-2xx response → "status":"down" with latency_ms: null (and the HTTP code when the server did answer).
  • /metrics is best-effort: if only that call fails, the check still counts as up and requests is null.
  • After 3 consecutive failures it prints an ALERT: line to stdout (once per outage), and a RECOVERED: line when the service answers again.

Single check (useful for cron, CI or a smoke test — exits 0 if up, 1 if down):

APP_URL="https://ghaymah-api-615e99f13665.hosted.ghaymah.systems" python q1-deploy-monitor/monitor/monitor.py --once

Leave the loop running well before the submission so the dashboard has real history.

Tuning via environment variables

Variable Default Meaning
APP_URL (required) base URL of the deployed app (also settable with --url)
INTERVAL_S 30 seconds between checks
TIMEOUT_S 5 per-request timeout
ALERT_AFTER 3 consecutive failures before the ALERT line
DATA_FILE monitor/data/checks.json where records are written
MAX_RECORDS 2880 rolling window (24 h at one check / 30 s)

Records are written atomically (temp file + rename), so the dashboard never reads a half-written file.


5. Open the dashboard

The page fetches ../monitor/data/checks.json, so it must be served over HTTP — opening index.html directly from the filesystem is blocked by the browser's file:// fetch restrictions (the page detects this and tells you so instead of failing silently).

cd q1-deploy-monitor && python -m http.server 8000

Then open http://localhost:8000/dashboard/index.html.

It shows:

  • Status badge — green UP / red DOWN from the most recent check, with the HTTP code and timestamp.
  • Total requestsrequests_total from the latest check.
  • Latest latency + the average across the stored window.
  • Uptime % across all stored checks.
  • Latency line chartlatency_ms over time (last 120 checks); down checks appear as gaps with red points.
  • Last-updated timestamp, auto-refreshing every 30 s.

With no data (monitor not started yet, file missing, or an empty/corrupt array) it renders a "no data yet" state and an explanatory banner rather than erroring.

To point the page at a different data file, edit the one constant at the top of the <script> block:

const DATA_URL = '../monitor/data/checks.json';

6. Verification performed

Locally

Check Result
pip install -r requirements.txt (pinned versions) fastapi 0.115.6, uvicorn 0.34.0 installed cleanly
GET /health HTTP 200 · {"status":"ok","release":"local","uptime_s":7.797,"timestamp":"..."}
GET / and GET /metrics valid JSON; requests_total increments per request
monitor.py --once against the running app UP code=200 latency=45.8ms requests=5, exit 0
monitor.py loop across an app shutdown up records → down records → ALERT printed on the 3rd consecutive failure
Dashboard against real checks.json badge, tiles, chart and uptime % all rendered; no console errors
Dashboard with the data file removed "no data yet" state + banner, no crash
docker build image builds clean (249 MB)
docker run container reports (healthy) via the HEALTHCHECK; all three endpoints respond

Against the Ghaymah deployment

Final release checked on 2026-07-26 at 20:46:07 UTC:

Endpoint Result
/health HTTP 200 · status=ok; release 60b9e2c38e3f1a509383c6d61a7f76b9d043214a
/ Service metadata and the same immutable release identifier
/metrics Valid request counter and process start time; the counter reflects real monitor traffic

After the endpoint returned HTTP 200 at 2026-07-26 20:46:07 UTC, the monitor was started with DATA_FILE=/data/checks.json, where /data is a read-write bind to the repository's monitor/data directory. Its /monitor code bind remains read-only. This lets atomic JSON updates append directly to the submitted history without allowing the container to alter its monitor source code. The older ghaymah-q1-monitor-history volume is deliberately not copied into the repository because it contains checks made while the free-tier service was hibernated.