Repository navigation
Deployment
One container image, one PostgreSQL database. Nothing else is required.
Published to ghcr.io/reportmate/reportmate-api on every push to main and
every v* tag, after the test job passes. Tags follow the branch, the PR, the semver version and
major.minor, the long commit SHA, and latest on the default branch. Built
for linux/amd64.
The base is python:3.11-slim, it runs as an unprivileged user (uid 1000),
and it listens on port 8000 under a single uvicorn worker. A HEALTHCHECK
curls /api/v1/health every 30 seconds.
DATABASE_URL=postgresql://reportmate:<password>@db.example.com:5432/reportmate
REPORTMATE_PASSPHRASE=<fleet passphrase>
Add DB_SSL=false only if your database does not speak TLS. Everything else
is in Configuration.
services:
api:
image: ghcr.io/reportmate/reportmate-api:latest
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://reportmate:password@db:5432/reportmate
DB_SSL: "false"
REPORTMATE_PASSPHRASE: changeme
CORS_ORIGINS: https://reportmate.example.com
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: reportmate
POSTGRES_PASSWORD: password
POSTGRES_DB: reportmate
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
init.sql is the deployment schema that creates devices, events and the
module tables, which the API's own migrations do not; take it from
schema/init.sql in selfhosted-docker-reportmate. See Data-Model.
A fuller stack, including the dashboard, is in selfhosted-docker-reportmate. Managed deployments are in terraform-azurerm-reportmate and terraform-aws-reportmate.
Alembic runs upgrade head from the application lifespan on every start. You
do not run a migration step separately, and rolling a new revision out is just
deploying the new image.
Two consequences worth knowing:
- Migration failure is logged, not fatal. The app still starts and can still serve reads. The readiness probe, not the migration, is the source of truth for whether the database is usable — so watch for
[STARTUP] Migrations failed:in the logs rather than assuming a healthy container means a migrated schema. - Every replica runs it, and nothing coordinates replicas. The statements use
IF NOT EXISTS/IF EXISTSso a repeat is a no-op, but a rolling deploy of N replicas means N attempts.
Migration 0009 swaps the partial unique index on events(device_id, module_id) for a plain one, and 0010 and 0011 create small tables; none
of them rewrites existing rows.
Migration 0008 drops twelve GIN indexes. A plain DROP INDEX is a catalog
change plus a file unlink — it takes an ACCESS EXCLUSIVE lock for
milliseconds and needs no rebuild, so it is safe inside the startup
transaction even on a large database.
| Path | Use it for | Behaviour |
|---|---|---|
/api/v1/health/live |
Liveness | Always 200 while the process serves. Checks nothing, so a transient database outage does not get healthy replicas killed. |
/api/v1/health/ready |
Readiness | 200 when a SELECT 1 succeeds, 503 otherwise. Point the load balancer here. |
/api/v1/health |
Public status | Reports database connectivity and version, 500 when unreachable. |
All three are anonymous by design — they have to work before secrets are mounted.
If the API sits behind Front Door, an ALB, or a Container Apps ingress, make
sure X-Forwarded-For is set. request.client.host is then one of a handful
of backend addresses carrying the entire fleet, and both the rate limiter and
the ingest-failure record read the first hop of X-Forwarded-For instead.
Without it every rate-limit bucket collapses and every recorded failure looks
like it came from the same four machines.
Set CORS_ORIGINS to the dashboard's origin. It is empty by default, so
cross-origin browser calls are refused until you configure it.
The image runs a single uvicorn worker; scale by adding replicas, not workers. Two things are per-replica and not shared:
- The response cache is in-process, so replicas can serve results up to their TTL apart.
- The rate limiter counts in process memory, so the effective fleet-wide limit is roughly the configured limit times the replica count.
Size DB_POOL_MAX (default 40) against your database's max_connections
divided by the replica count. The default is matched to Starlette's threadpool
so a full house of workers can each hold one connection.
GET /api/v1/metrics serves Prometheus exposition and requires a credential,
so your scraper needs a read-scoped API key. It is excluded from the OpenAPI
schema. Series exposed:
| Metric | Type | Labels |
|---|---|---|
reportmate_http_requests_total |
counter |
method, path, status
|
reportmate_http_request_duration_seconds |
histogram |
method, path
|
reportmate_http_requests_in_progress |
gauge | none |
path is the matched route template, never the raw path, so a fleet of serial
numbers cannot explode label cardinality. Unmatched requests are labelled
unmatched.
Optional. Set EVENTS_CONNECTION to an Azure Web PubSub connection string and
the API broadcasts each stored event to the events hub, with
GET /api/v1/negotiate issuing 60-minute client tokens. Without it the
dashboard falls back to polling; nothing errors.
AGPL-3.0 — a commercial license is available. reportmate.app