-
Notifications
You must be signed in to change notification settings - Fork 1
Deployment and Operations
This is the reference half of deployment: topology, routes, configuration keys, schema ownership, health and the background job catalog.
The procedures — install, upgrade, backup, troubleshooting — live with the scripts they drive, in the TrackHub.Deployment repository:
| Document | Use it for |
|---|---|
QUICKSTART.md |
A first installation, end to end |
INSTALL.md |
The full guide: detailed installation, configuration reference, upgrades, SSL, monitoring, troubleshooting |
README.md |
Script inventory and day-to-day commands |
Related pages: Database · Technology · Frontend
Everything runs in Docker behind a single nginx reverse proxy that terminates SSL and routes by path.
graph TB
net["Internet"] --> nginx["nginx<br/><i>80 / 443, SSL termination</i>"]
nginx -->|/| fe["Frontend<br/><i>static SPA</i>"]
nginx -->|/Identity/| auth["Authority :8080"]
nginx -->|/Security/| sec["Security :8080"]
nginx -->|/Manager/| mgr["Manager :8080"]
nginx -->|/Router/| rtr["Router :8080"]
nginx -->|/Geofence/| geo["Geofencing :8080"]
nginx -->|/Trip/| trip["TripManagement :8080"]
nginx -->|/Reporting/| rpt["Reporting :8080"]
nginx -->|/Telemetry/| tele["Telemetry :8080"]
sync["SyncWorker<br/><i>no listener</i>"]
dbinit["db-init<br/><i>one-shot seeder</i>"]
pg[("PostgreSQL 14+<br/><i>external, PostGIS</i>")]
auth & sec & mgr & rtr & geo & trip & rpt & tele --> pg
sync --> rtr
dbinit --> pg
| Service | Path | Container port | Notes |
|---|---|---|---|
| Frontend | / |
— | React SPA, including the anonymous /status page |
| Authority | /Identity/ |
8080 | OpenIddict identity server |
| Security | /Security/ |
8080 | Users and permissions (GraphQL) |
| Manager | /Manager/ |
8080 | Master data (GraphQL) + one anonymous REST endpoint, api/PlatformStatus/announcements
|
| Router | /Router/ |
8080 | Device routing (GraphQL + REST) |
| Geofencing | /Geofence/ |
8080 | Geofence management (GraphQL) |
| TripManagement | /Trip/ |
8080 | Trips, routes and tolls (GraphQL). Requires an OpenRouteService key |
| Reporting | /Reporting/ |
8080 | Report generation (REST) |
| Telemetry | /Telemetry/ |
8080 | Position and telemetry store (GraphQL) |
| SyncWorker | — | — | Background sync; built from the TrackHubRouter repository, no HTTP listener
|
| nginx | — | 80 / 443 | Reverse proxy, SSL termination |
| db-init | — | — | One-shot seeder — data only, never schema |
In local development the services listen on their own HTTPS ports; TripManagement, the newest service, uses https://localhost:5006 / http://localhost:5007.
| Command | Result |
|---|---|
./scripts/deploy.sh full |
Frontend + all backend services (default) |
./scripts/deploy.sh frontend |
Frontend only |
./scripts/deploy.sh backend |
Backend services only |
Builds use the Docker layer cache and reliably detect source changes, so updated code always deploys without --no-cache. Containers start with --force-recreate so freshly built images take effect, and the frontend refreshes its static assets on every start.
All backend services share a similar appsettings.json shape, generated from a central .env into generated/ and bind-mounted read-only over each container's /app/appsettings.json. deploy.sh regenerates them before every backend or full deploy. Environment variables set in compose still take precedence over the mounted file.
| Key | Purpose |
|---|---|
ConnectionStrings:DefaultConnection |
The service's PostgreSQL connection |
DB_CONNECTION_TELEMETRY |
Must point at the same TrackHub database as Manager — Telemetry does not own its schema |
DB_CONNECTION_LOGGING |
Centralized Serilog sink; the logs table is auto-created on first write |
AuthorityServer:* |
Authority base URL, ClientId, ClientSecret, IsService
|
AppSettings:GraphQL{Service}Service |
Each downstream GraphQL endpoint (Manager, Telemetry, Router, Geofence, TripManagement, Identity) |
AppSettings:AuthorizationCacheSeconds |
Authorization decision cache window (default 30; 0 disables) |
AppSettings:Protocols |
The GPS provider protocols to register — Router and SyncWorker |
AppSettings:MaxConcurrentOperatorSyncs |
Global operator concurrency gate (default 10) |
AppSettings:DeviceCatalogCacheSeconds |
Device catalog cache (default 60) |
AppSettings:Reporting |
MaxExportRows 100 000, MaxPdfRows 500, PreviewRows 100 |
AppSettings:Routing |
OpenRouteService provider, base URL, API key, profile, rate limit, timeout, max waypoints |
AppSettings:Smtp / AppSettings:WhatsApp
|
Notification channel credentials |
AppSettings:NotificationDeliveryRetentionDays |
Default 90 |
AllowedCorsOrigins |
Browser origins permitted per service |
REACT_APP_* |
Frontend endpoints — see Frontend |
Seeded OAuth client secrets, sample appsettings, .env examples and DBInitializer seed data are intentionally committed so a multi-service dev environment stands up without manual secret plumbing. Production overrides every one of them through environment variables. See Security and Identity.
EF Core migrations are the source of truth for schema. The db-init container seeds data only — it does not create or migrate anything. Migrations must be applied for new installations and updates, before deploying the updated services.
| Migration project | Database | Schemas it creates |
|---|---|---|
| TrackHubSecurity | TrackHubSecurity |
security |
| TrackHub.AuthorityServer | TrackHubSecurity |
OpenIddict tables |
| TrackHub.Manager | TrackHub |
app, map, and telemetry
|
| TrackHub.Geofencing | TrackHub |
geofencing (PostGIS) |
| TrackHub.TripManagement | TrackHub |
trip (PostGIS) |
Telemetry has no migrations of its own. Its tables come from the Manager migrations.
PostgreSQL needs PostGIS for the Geofencing and TripManagement schemas. One
CREATE EXTENSION postgiscovers both — they share theTrackHubdatabase.
The migration host needs the .NET SDK and
dotnet-ef. TheTrackHubCommon.*packages are not on nuget.org, so pack them from source into a local feed and register it before runningdotnet ef. Docker image builds do this automatically in acommonstage. See Common Library.
A migration creates the schema but seeds nothing. Without the new service client registration and the resource/role seeding, every call to the new module returns FORBIDDEN while the service reports healthy. This is the single most common post-upgrade symptom.
| Surface | What it tells you |
|---|---|
GET {service}/health |
Anonymous per-service probe, mapped by every backend |
/status in the portal |
The public status page — service tiles, announcements, and (for privileged users) sync activity and the background-jobs table |
./scripts/health-check.sh <domain> |
Scripted rollup |
The status page probes each service from the browser, not from a server rollup, so it works when a user cannot sign in. If nginx or the frontend itself is down, nothing can self-report — that is accepted. See Frontend.
Publish a maintenance announcement before taking Manager down. Announcements are stored and served by Manager, so a Manager outage removes the banner while the tiles keep working.
The audited catalog is Common.Domain.Constants.BackgroundJobKeys — in Common because Geofencing also records runs through Manager's createBackgroundJobRun.
| Job key | Host | Interval |
|---|---|---|
alert-evaluation |
Manager | 5 min |
notification-dispatch |
Manager | 30 s |
notification-digest |
Manager | hourly |
delivery-retention |
Manager | daily |
document-scan |
Manager | — |
document-expiration |
Manager | — |
document-retention-cleanup |
Manager | — |
workforce-expiration-scan |
Manager | — |
trial-expiration |
Manager | — |
platform-retention |
Manager | — |
geofence-dwell-evaluation |
Geofencing | 60 s |
trip-eta-refresh |
TripManagement | 5 min |
trip-schedule-reminder |
TripManagement | 15 min |
Staleness is only ever asserted for verified per-cycle jobs. An audit of this catalog found that
alert-evaluationis the only key with a guaranteed recording floor, and that floor is daily — not per-cycle. Every other job records a run only when it actually did work, so an old timestamp there is the healthy steady state and renders neutrally as "idle" on the status page. Do not "fix" that as a bug.
Every recurring job except the Router's SyncWorker runs as a hosted service inside its own service's web host. The SyncWorker is the only standalone worker process — and it is single-instance by design: IOperatorSyncLock, ExecutionIntervalManager and IProviderSessionStore are all in-process. Scaling it out would need a cross-instance claim (a PostgreSQL advisory lock or SKIP LOCKED).
Uploaded documents are not in the database. They live on the manager-documents volume (or in S3 / Azure Blob), capped at 50 MB per upload by nginx.
-
backup-database.shdoes not cover them — back that volume up separately. -
Never run
docker compose down -v— it deletes them.
That volume is the only stateful data outside PostgreSQL.
Centralized logging writes to a logs table in the TrackHub database via a shared Serilog sink, configured through DB_CONNECTION_LOGGING. The sink auto-creates the table on first write.
x-correlation-id propagates across every inter-service hop, so one user action can be traced end to end. When a GraphQL call returns the masked "Unexpected Execution Error", the real exception is in the producing service's log — or run that binary with ASPNETCORE_ENVIRONMENT=Development on a spare port to unmask it.
- Docker 24.0+ and Docker Compose v2.20+
- PostgreSQL 14+ (external), with PostGIS
- An SSL certificate and a domain name
- An OpenRouteService API key, if TripManagement is deployed
Platform
- Architecture
- Technology
- Inter-Service Communication
- Database
- Security and Identity
- User Permissions Overview
Services
- Manager
- Telemetry
- Router
- Adding a Provider
- Geofencing
- Trip Management
- Reporting
- Common Library
- Frontend
Engineering
User docs
- User Guide (ships in the app)