Direct VPS Backend Deployment¶
Status: Deferred migration target; not the live production scheduler path Last Updated: 2026-04-16
This document defines the app-local deployment path for moving the Wait Time Canada backend scheduler/runtime from GitHub Actions onto the shared VPS.
Shared-VPS ownership note:
- Shared host topology, ingress ownership, release-root conventions, and live cross-project inventory are canonical in
/home/jer/repos/vps/platform-ops. - Use
/home/jer/repos/vps/platform-ops/docs/standards/PLAT-009-shared-vps-documentation-boundary.mdfor the documentation boundary. - This repo remains canonical for Wait Time Canada's backend packaging, scheduler units, deploy, verify, and rollback steps.
Current State¶
As of 2026-04-16:
- production scraper scheduling still runs on GitHub Actions
- heartbeat monitoring still runs on GitHub Actions
- the managed Neon database remains the production data plane
- the direct-VPS backend path described here was staged and verified mechanically, but it is not live because the Ontario source timed out repeatedly from the shared VPS during cutover testing
- the
waittime-backend-*timers on the VPS should remain disabled unless a later investigation resolves the Ontario reachability problem
Target Runtime Shape¶
The VPS target is a Python worker release with systemd timers:
- release root:
/srv/apps/waittime-backend - current symlink:
/srv/apps/waittime-backend/current - backend working directory:
/srv/apps/waittime-backend/current/backend - env file:
/etc/projects-merge/env/waittime-backend.env - shared Playwright browser cache:
/srv/apps/waittime-backend/shared/playwright-browsers - timers:
waittime-backend-scraper.timerwaittime-backend-heartbeat.timerwaittime-backend-quality-snapshot.timer- optional timer:
waittime-backend-database-cleanup.timer
The database remains managed in Neon for this wave.
Current Blocker¶
The attempted VPS backend cutover was paused after repeated failures reaching https://www.hqontario.ca/system-performance/time-spent-in-emergency-departments from the shared VPS:
- Playwright navigation timed out from this host
- plain
httpxfetches timed out from this host - plain
curlfrom the VPS also timed out
Operational meaning:
- this is not currently treated as a packaging or deploy-script problem
- GitHub Actions remains the live scheduler path because it can still reach the Ontario source
- do not re-enable the VPS timers until an Ontario-compatible runtime path is proven
Ontario Reachability Diagnostic¶
Before revisiting backend cutover on any VPS-like host, record a reproducible Ontario reachability check using the current source URL:
curl -I -L -sS -o /dev/null \
-w 'connect=%{time_connect} tls=%{time_appconnect} start=%{time_starttransfer} total=%{time_total}\n' \
https://www.hqontario.ca/system-performance/time-spent-in-emergency-departments
cd /srv/apps/waittime-backend/current/backend
./.venv/bin/python - <<'PY'
import httpx
from waittime.scrapers.observability import (
DEFAULT_HTTP_CONNECT_TIMEOUT_SECONDS,
DEFAULT_HTTP_POOL_TIMEOUT_SECONDS,
DEFAULT_HTTP_READ_TIMEOUT_SECONDS,
DEFAULT_HTTP_WRITE_TIMEOUT_SECONDS,
)
timeout = httpx.Timeout(
connect=DEFAULT_HTTP_CONNECT_TIMEOUT_SECONDS,
read=DEFAULT_HTTP_READ_TIMEOUT_SECONDS,
write=DEFAULT_HTTP_WRITE_TIMEOUT_SECONDS,
pool=DEFAULT_HTTP_POOL_TIMEOUT_SECONDS,
)
url = "https://www.hqontario.ca/system-performance/time-spent-in-emergency-departments"
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
response = client.get(url)
print({"status_code": response.status_code, "bytes": len(response.text)})
PY
If Chromium is available, optionally run a one-off Playwright navigation from the same host and record whether it succeeds or times out. Treat the result as cutover evidence, not as a one-time anecdote.
Required Env Contract¶
Required:
DATABASE_URL
Recommended:
ENVIRONMENT=productionLOG_LEVEL=INFOHEARTBEAT_STALE_THRESHOLD_MINUTES=120ALERTS_ENABLED=truePUSHOVER_USER_KEYPUSHOVER_API_TOKENPLAYWRIGHT_BROWSERS_PATH=/srv/apps/waittime-backend/shared/playwright-browsers
Optional:
MAPBOX_TOKENfor geocoding enrichment when new hospitals are discoveredSENTRY_DSNALERTS_REFERENCE_URLif alert notifications should point to a VPS-specific runbook or dashboard instead of the current GitHub Actions view
Host Prerequisites¶
Install runtime dependencies on the VPS before the first backend deploy:
Playwright note:
- the deploy script installs Chromium into the shared Playwright cache
- the host still needs the Chromium runtime libraries that Playwright expects
- if they are missing, run:
cd /srv/apps/waittime-backend/current/backend
PLAYWRIGHT_BROWSERS_PATH=/srv/apps/waittime-backend/shared/playwright-browsers \
./.venv/bin/playwright install --with-deps chromium
Packaging Files¶
The direct-VPS backend path uses:
scripts/deploy-vps-backend.shscripts/release-vps-backend.shscripts/install-vps-backend-systemd.shscripts/verify-vps-backend.shbackend/systemd/
Local Preflight¶
Before preparing a backend release:
cd /home/jer/repos/vps/waittimecanada/backend
python -m pytest tests/unit/test_scraper_cli.py \
tests/unit/test_check_heartbeat_cli.py \
tests/unit/test_cleanup_cli.py \
tests/unit/test_snapshot_quality_cli.py
ruff check src tests
mypy src
Deploy On The VPS¶
From the checked-out release on the VPS:
cd /srv/apps/waittime-backend/current
./scripts/deploy-vps-backend.sh /etc/projects-merge/env/waittime-backend.env
sudo ./scripts/install-vps-backend-systemd.sh --enable
The deploy script:
- creates or refreshes
backend/.venv - installs the backend package into that venv
- installs Chromium into the shared Playwright cache
- applies database migrations using the provided env file
The systemd installer:
- installs the timer/service templates from
backend/systemd/ - substitutes the runtime user and group
- reloads systemd
- enables scraper, heartbeat, and quality snapshot timers
- optionally enables the cleanup timer with
--enable-cleanup
Cleanup note:
- the shipped cleanup timer skips aggregate refresh and deletes raw measurements older than 30 days in bounded batches
- the optional timer therefore restores the repository's storage-safety policy without turning maintenance into a long-running catch-all job
Stage And Release From A Workstation¶
Defaults:
- app root:
/srv/apps/waittime-backend - env file:
/etc/projects-merge/env/waittime-backend.env
Verification¶
After deploy and timer installation:
sudo ./scripts/verify-vps-backend.sh
sudo systemctl list-timers 'waittime-backend-*' --all
sudo journalctl -u waittime-backend-scraper.service -n 50 --no-pager
sudo journalctl -u waittime-backend-heartbeat.service -n 50 --no-pager
Expected outcome:
- scraper, heartbeat, and quality snapshot timers are enabled and active
- the heartbeat dry-run completes successfully
- recent scraper runs write fresh
scraper_statusrows - alerting is configured if
PUSHOVER_*vars are present
Current reality:
- frontend verification passed, but backend verification failed on Ontario reachability from the VPS
- the timers were therefore disabled again
- this runbook remains useful for a future retry, but it is not the live production scheduler path today
Rollback¶
Rollback is release-based:
- identify the previous release in
/srv/apps/waittime-backend/releases - repoint
/srv/apps/waittime-backend/current - rerun
./scripts/deploy-vps-backend.sh /etc/projects-merge/env/waittime-backend.env - reload systemd and restart the timers:
sudo systemctl daemon-reload
sudo systemctl restart \
waittime-backend-scraper.timer \
waittime-backend-heartbeat.timer \
waittime-backend-quality-snapshot.timer
Cutover Rule¶
Do not disable the GitHub Actions backend schedulers until:
- the VPS scraper timer has completed successfully at least once
- the heartbeat timer verifies fresh rows on the VPS path
- rollback to GitHub Actions remains straightforward
- the Ontario reachability diagnostic above passes reproducibly on the target host
Decision gate:
- if the Ontario source still times out from the shared VPS, keep the timers disabled and do not reopen cutover
- only revisit cutover after a reproducible pass on the target host or after proving a different runtime path
As of 2026-04-16, keep GitHub Actions live and treat the VPS backend path as deferred.