Skip to content

Deployment and Operations

Sergio Hernandez edited this page Jul 24, 2026 · 1 revision

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


Topology

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
Loading
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.

Deployment shapes

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.


Configuration

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 configuration groups

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

Committed development secrets

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.


Migrations

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 postgis covers both — they share the TrackHub database.

The migration host needs the .NET SDK and dotnet-ef. The TrackHubCommon.* packages are not on nuget.org, so pack them from source into a local feed and register it before running dotnet ef. Docker image builds do this automatically in a common stage. See Common Library.

After adding a service or module, re-run db-init

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.


Health and status

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.


Background jobs

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-evaluation is 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).


State outside PostgreSQL

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.sh does 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.


Logging

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.


Requirements

  • 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

Clone this wiki locally