# 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":"","uptime_s":12.34,"timestamp":"2026-07-26T20:46:06.847603+00:00"}` — HTTP 200 | | `GET` | `/metrics` | `{"requests_total":42,"started_at":""}` | | `GET` | `/docs` | interactive OpenAPI docs (FastAPI built-in) | Example `/` response: ```json { "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 ```bash 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 ``` ```bash curl http://localhost:8080/health ``` --- ## 2. Build and run the Docker image locally ```bash docker build -t ghaymah-api:latest ./q1-deploy-monitor/app ``` ```bash docker run --rm -p 8080:8080 --name ghaymah-api ghaymah-api:latest ``` Verify: ```bash 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)`: ```bash 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 ```bash docker login ``` ```bash docker tag ghaymah-api:latest docker.io/agamy74/ghaymah-api:latest ``` ```bash 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: > ```bash > 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:` | | Application Name | `ghaymah-api` | | Port Number | `8080` (must match the `EXPOSE`d 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 ```bash curl -f https://ghaymah-api-615e99f13665.hosted.ghaymah.systems/health ``` Expected: HTTP 200 with `{"status":"ok","release":"","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. ```bash export APP_URL="https://ghaymah-api-615e99f13665.hosted.ghaymah.systems" python q1-deploy-monitor/monitor/monitor.py ``` On Windows PowerShell: ```bash $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: ```json { "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: ```json { "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): ```bash 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). ```bash cd q1-deploy-monitor && python -m http.server 8000 ``` Then open . It shows: - **Status badge** — green `UP` / red `DOWN` from the most recent check, with the HTTP code and timestamp. - **Total requests** — `requests_total` from the latest check. - **Latest latency** + the average across the stored window. - **Uptime %** across all stored checks. - **Latency line chart** — `latency_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 `